1use crate::config::ConfigSet;
29use crate::error::{Error, Result};
30use crate::fs_publish::publish_no_clobber;
31use crate::objects::ObjectId;
32use crate::refs::store::RefStoreError;
33use memmap2::Mmap;
34use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
35use std::fs;
36use std::io::Write;
37use std::ops::ControlFlow;
38use std::path::{Path, PathBuf};
39use std::sync::{Arc, Mutex, OnceLock};
40use std::time::{Duration, Instant};
41use tempfile::TempPath;
42
43const REFTABLE_MAGIC: &[u8; 4] = b"REFT";
49
50const HEADER_SIZE: usize = 24;
53
54const FOOTER_V1_SIZE: usize = 68;
56
57const BLOCK_TYPE_REF: u8 = b'r';
59const BLOCK_TYPE_INDEX: u8 = b'i';
61const BLOCK_TYPE_LOG: u8 = b'g';
63const BLOCK_TYPE_OBJ: u8 = b'o';
65
66const VALUE_DELETION: u8 = 0;
68const VALUE_ONE_OID: u8 = 1;
69const VALUE_TWO_OID: u8 = 2;
70const VALUE_SYMREF: u8 = 3;
71
72const HASH_SIZE: usize = 20;
74
75const DEFAULT_BLOCK_SIZE: u32 = 4096;
77
78const RESTART_INTERVAL: usize = 16;
80
81#[derive(Debug, thiserror::Error)]
87#[non_exhaustive]
88pub enum ReftableError {
89 #[error("reftable: file is too small ({len} bytes)")]
91 TooSmall {
92 len: usize,
94 },
95
96 #[error("reftable: bad magic bytes")]
98 BadMagic,
99
100 #[error("reftable: unsupported format version {version}")]
102 UnsupportedVersion {
103 version: u8,
105 },
106
107 #[error("reftable: footer version {footer} does not match header version {header}")]
109 FooterVersionMismatch {
110 header: u8,
112 footer: u8,
114 },
115
116 #[error("reftable: footer checksum mismatch (stored {stored:08x}, computed {computed:08x})")]
118 FooterChecksumMismatch {
119 stored: u32,
121 computed: u32,
123 },
124
125 #[error("reftable: truncated {field}")]
127 Truncated {
128 field: ReftableField,
130 },
131
132 #[error("reftable: {field} is not valid UTF-8")]
134 InvalidUtf8 {
135 field: ReftableField,
137 },
138
139 #[error("reftable: {field} prefix is longer than the previous key")]
141 PrefixTooLong {
142 field: ReftableField,
144 },
145
146 #[error("reftable: unknown ref value type {value_type}")]
148 UnknownValueType {
149 value_type: u8,
151 },
152
153 #[error("reftable: invalid block length")]
155 InvalidBlockLength,
156
157 #[error("reftable: invalid restart table")]
159 InvalidRestartTable,
160
161 #[error("reftable: malformed log record key")]
163 InvalidLogKey,
164
165 #[error("reftable: tables.list names missing table {name}")]
167 MissingTable {
168 name: String,
170 },
171
172 #[error("reftable: refs must be added in sorted order ('{next}' after '{previous}')")]
174 UnsortedRefs {
175 previous: String,
177 next: String,
179 },
180
181 #[error("reftable: record does not fit in a block")]
183 EntryTooLarge,
184
185 #[error("reftable: record has an empty key")]
187 EmptyRecordKey,
188
189 #[error("reftable: stack is locked by another writer ({})", lock_path.display())]
191 Locked {
192 lock_path: PathBuf,
194 },
195
196 #[error("reftable: tables.list lock was already released")]
198 LockReleased,
199
200 #[error(
202 "reftable: '{refname}' expected {expected} but found {}",
203 actual.map_or_else(|| "<absent>".to_owned(), |oid| oid.to_hex())
204 )]
205 ExpectedMismatch {
206 refname: String,
208 expected: ObjectId,
210 actual: Option<ObjectId>,
212 },
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217#[non_exhaustive]
218pub enum ReftableField {
219 Varint,
221 BlockHeader,
223 IndexKey,
225 RefName,
227 ObjectId,
229 ObjectIdPair,
231 SymrefTarget,
233 LogKey,
235 LogName,
237 LogEmail,
239 LogTimezone,
241 LogMessage,
243 Footer,
245}
246
247impl std::fmt::Display for ReftableField {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 f.write_str(match self {
250 Self::Varint => "varint",
251 Self::BlockHeader => "block header",
252 Self::IndexKey => "index key",
253 Self::RefName => "ref name",
254 Self::ObjectId => "object id",
255 Self::ObjectIdPair => "object id pair",
256 Self::SymrefTarget => "symref target",
257 Self::LogKey => "log key",
258 Self::LogName => "log name",
259 Self::LogEmail => "log email",
260 Self::LogTimezone => "log timezone offset",
261 Self::LogMessage => "log message",
262 Self::Footer => "footer",
263 })
264 }
265}
266
267#[cold]
268#[inline(never)]
269fn truncated(field: ReftableField) -> Error {
270 ReftableError::Truncated { field }.into()
271}
272
273#[cold]
274#[inline(never)]
275fn invalid_utf8(field: ReftableField) -> Error {
276 ReftableError::InvalidUtf8 { field }.into()
277}
278
279#[cold]
280#[inline(never)]
281fn corrupt(err: ReftableError) -> Error {
282 err.into()
283}
284
285fn put_varint(mut val: u64, out: &mut Vec<u8>) -> usize {
291 let mut buf = [0u8; 10];
293 let mut i = 0;
294 buf[i] = (val & 0x7f) as u8;
295 i += 1;
296 val >>= 7;
297 while val > 0 {
298 val -= 1;
299 buf[i] = (val & 0x7f) as u8;
300 i += 1;
301 val >>= 7;
302 }
303 let len = i;
305 for j in (1..len).rev() {
306 out.push(buf[j] | 0x80);
307 }
308 out.push(buf[0]);
309 len
310}
311
312fn get_varint(data: &[u8], mut pos: usize) -> Result<(u64, usize)> {
314 if pos >= data.len() {
315 return Err(truncated(ReftableField::Varint));
316 }
317 let mut val = (data[pos] & 0x7f) as u64;
318 while data[pos] & 0x80 != 0 {
319 pos += 1;
320 if pos >= data.len() {
321 return Err(truncated(ReftableField::Varint));
322 }
323 val = ((val + 1) << 7) | (data[pos] & 0x7f) as u64;
324 }
325 Ok((val, pos + 1))
326}
327
328#[derive(Debug, Clone, PartialEq, Eq)]
334pub enum RefValue {
335 Deletion,
337 Val1(ObjectId),
339 Val2(ObjectId, ObjectId),
341 Symref(String),
343}
344
345#[derive(Debug, Clone)]
347pub struct RefRecord {
348 pub name: String,
350 pub update_index: u64,
352 pub value: RefValue,
354}
355
356#[derive(Debug, Clone)]
358pub struct LogRecord {
359 pub refname: String,
361 pub update_index: u64,
363 pub old_id: ObjectId,
365 pub new_id: ObjectId,
367 pub name: String,
369 pub email: String,
371 pub time_seconds: u64,
373 pub tz_offset: i16,
376 pub message: String,
378}
379
380#[must_use]
382pub(crate) fn log_record_is_deletion(record: &LogRecord) -> bool {
383 record.name.is_empty()
384 && record.email.is_empty()
385 && record.message.is_empty()
386 && record.time_seconds == 0
387 && record.tz_offset == 0
388 && record.old_id.is_zero()
389 && record.new_id.is_zero()
390}
391
392#[derive(Debug, Clone)]
394pub struct WriteOptions {
395 pub block_size: u32,
397 pub restart_interval: usize,
399 pub write_log: bool,
401 pub skip_index_objects: bool,
403 pub unpadded: bool,
405 pub hash_size: usize,
408 pub auto_compaction_factor: u8,
411 pub autocompaction_enabled: bool,
414}
415
416impl Default for WriteOptions {
417 fn default() -> Self {
418 Self {
419 block_size: DEFAULT_BLOCK_SIZE,
420 restart_interval: RESTART_INTERVAL,
421 write_log: true,
422 skip_index_objects: false,
423 unpadded: false,
424 hash_size: HASH_SIZE,
425 auto_compaction_factor: 2,
426 autocompaction_enabled: true,
427 }
428 }
429}
430
431#[derive(Debug, Clone)]
438pub struct ReftableTransactionUpdate {
439 pub refname: String,
441 pub value: Option<RefValue>,
443 pub log: Option<LogRecord>,
445 pub expected_old: Option<ObjectId>,
448}
449
450pub struct ReftableWriter {
464 opts: WriteOptions,
465 min_update_index: u64,
466 max_update_index: u64,
467
468 refs: Vec<RefRecord>,
470 logs: Vec<LogRecord>,
472}
473
474impl ReftableWriter {
475 pub fn new(opts: WriteOptions, min_update_index: u64, max_update_index: u64) -> Self {
477 Self {
478 opts,
479 min_update_index,
480 max_update_index,
481 refs: Vec::new(),
482 logs: Vec::new(),
483 }
484 }
485
486 pub fn add_ref(&mut self, rec: RefRecord) -> Result<()> {
488 if let Some(last) = self.refs.last() {
489 if rec.name <= last.name {
490 return Err(ReftableError::UnsortedRefs {
491 previous: last.name.clone(),
492 next: rec.name,
493 }
494 .into());
495 }
496 }
497 self.refs.push(rec);
498 Ok(())
499 }
500
501 pub fn add_log(&mut self, rec: LogRecord) -> Result<()> {
503 self.logs.push(rec);
504 Ok(())
505 }
506
507 pub fn finish(self) -> Result<Vec<u8>> {
513 let refs = self.refs;
514 let logs = self.logs;
515 let opts = self.opts;
516 let mut w = WriterState::new(opts, self.min_update_index, self.max_update_index);
517
518 for rec in &refs {
520 w.add_ref(rec)?;
521 }
522
523 let mut logs = logs;
526 logs.sort_by(|a, b| {
527 a.refname
528 .cmp(&b.refname)
529 .then_with(|| b.update_index.cmp(&a.update_index))
530 });
531 if w.opts.write_log {
532 for log in &logs {
533 w.add_log(log)?;
534 }
535 }
536
537 w.close()
538 }
539}
540
541const REFTABLE_DEFAULT_BLOCK_SIZE: u32 = 4096;
547const MAX_RESTARTS: usize = (1 << 16) - 1;
549
550enum EncRecord<'a> {
552 Ref(&'a RefRecord, u64),
553 Log(&'a LogRecord),
554 Obj { prefix: Vec<u8>, offsets: Vec<u64> },
555 Index { last_key: Vec<u8>, offset: u64 },
556}
557
558impl EncRecord<'_> {
559 fn block_type(&self) -> u8 {
560 match self {
561 EncRecord::Ref(..) => BLOCK_TYPE_REF,
562 EncRecord::Log(_) => BLOCK_TYPE_LOG,
563 EncRecord::Obj { .. } => BLOCK_TYPE_OBJ,
564 EncRecord::Index { .. } => BLOCK_TYPE_INDEX,
565 }
566 }
567
568 fn key(&self) -> Vec<u8> {
570 match self {
571 EncRecord::Ref(r, _) => r.name.as_bytes().to_vec(),
572 EncRecord::Log(l) => {
573 let mut k = Vec::with_capacity(l.refname.len() + 9);
574 k.extend_from_slice(l.refname.as_bytes());
575 k.push(0);
576 let ts = u64::MAX - l.update_index;
577 k.extend_from_slice(&ts.to_be_bytes());
578 k
579 }
580 EncRecord::Obj { prefix, .. } => prefix.clone(),
581 EncRecord::Index { last_key, .. } => last_key.clone(),
582 }
583 }
584
585 fn val_type(&self) -> u8 {
587 match self {
588 EncRecord::Ref(r, _) => match r.value {
589 RefValue::Deletion => VALUE_DELETION,
590 RefValue::Val1(_) => VALUE_ONE_OID,
591 RefValue::Val2(..) => VALUE_TWO_OID,
592 RefValue::Symref(_) => VALUE_SYMREF,
593 },
594 EncRecord::Log(l) => {
595 if log_record_is_deletion(l) {
596 VALUE_DELETION
597 } else {
598 1
599 }
600 }
601 EncRecord::Obj { offsets, .. } => {
602 if !offsets.is_empty() && offsets.len() < 8 {
603 offsets.len() as u8
604 } else {
605 0
606 }
607 }
608 EncRecord::Index { .. } => 0,
609 }
610 }
611
612 fn encode_value(&self, opts: &WriteOptions, out: &mut Vec<u8>) {
614 match self {
615 EncRecord::Ref(r, update_index_delta) => {
616 put_varint(*update_index_delta, out);
617 match &r.value {
618 RefValue::Deletion => {}
619 RefValue::Val1(oid) => out.extend_from_slice(oid.as_bytes()),
620 RefValue::Val2(oid, peeled) => {
621 out.extend_from_slice(oid.as_bytes());
622 out.extend_from_slice(peeled.as_bytes());
623 }
624 RefValue::Symref(target) => {
625 put_varint(target.len() as u64, out);
626 out.extend_from_slice(target.as_bytes());
627 }
628 }
629 }
630 EncRecord::Log(l) => {
631 if log_record_is_deletion(l) {
632 return;
633 }
634 out.extend_from_slice(l.old_id.as_bytes());
635 out.extend_from_slice(l.new_id.as_bytes());
636 put_varint(l.name.len() as u64, out);
637 out.extend_from_slice(l.name.as_bytes());
638 put_varint(l.email.len() as u64, out);
639 out.extend_from_slice(l.email.as_bytes());
640 put_varint(l.time_seconds, out);
641 out.extend_from_slice(&l.tz_offset.to_be_bytes());
642 let msg = clean_log_message(&l.message, opts);
643 put_varint(msg.len() as u64, out);
644 out.extend_from_slice(&msg);
645 }
646 EncRecord::Obj { offsets, .. } => {
647 if offsets.is_empty() || offsets.len() >= 8 {
648 put_varint(offsets.len() as u64, out);
649 }
650 if offsets.is_empty() {
651 return;
652 }
653 put_varint(offsets[0], out);
654 let mut last = offsets[0];
655 for &o in &offsets[1..] {
656 put_varint(o - last, out);
657 last = o;
658 }
659 }
660 EncRecord::Index { offset, .. } => {
661 put_varint(*offset, out);
662 }
663 }
664 }
665}
666
667fn clean_log_message(msg: &str, opts: &WriteOptions) -> Vec<u8> {
678 let limit = (opts.block_size as usize / 2).max(1);
685 let msg = if msg.len() > limit {
686 let mut end = limit;
687 while end > 0 && !msg.is_char_boundary(end) {
688 end -= 1;
689 }
690 &msg[..end]
691 } else {
692 msg
693 };
694 let trimmed = msg.trim_end_matches('\n');
695 let mut out = trimmed.as_bytes().to_vec();
696 out.push(b'\n');
697 out
698}
699
700fn encode_key(prev: &[u8], key: &[u8], extra: u8, out: &mut Vec<u8>) -> bool {
703 let prefix_len = common_prefix_len(prev, key);
704 let suffix_len = key.len() - prefix_len;
705 put_varint(prefix_len as u64, out);
706 put_varint(((suffix_len as u64) << 3) | (extra as u64), out);
707 out.extend_from_slice(&key[prefix_len..]);
708 prefix_len == 0
709}
710
711struct BlockWriter {
713 typ: u8,
714 buf: Vec<u8>,
717 header_off: usize,
718 block_size: usize,
719 restart_interval: usize,
720 restarts: Vec<u32>,
721 last_key: Vec<u8>,
722 entries: usize,
723}
724
725impl BlockWriter {
726 fn new(typ: u8, block_size: usize, header_off: usize, restart_interval: usize) -> Self {
727 let mut buf = vec![0u8; header_off + 4];
729 buf[header_off] = typ;
730 Self {
731 typ,
732 buf,
733 header_off,
734 block_size,
735 restart_interval,
736 restarts: Vec::new(),
737 last_key: Vec::new(),
738 entries: 0,
739 }
740 }
741
742 fn next(&self) -> usize {
745 self.buf.len()
746 }
747
748 fn add(&mut self, rec: &EncRecord, opts: &WriteOptions) -> Result<bool> {
751 let key = rec.key();
752 if key.is_empty() {
753 return Err(ReftableError::EmptyRecordKey.into());
754 }
755 let restart = self.entries.is_multiple_of(self.restart_interval);
756 let prev: &[u8] = if restart { &[] } else { &self.last_key };
757
758 let mut encoded = Vec::new();
759 let is_restart = encode_key(prev, &key, rec.val_type(), &mut encoded);
760 rec.encode_value(opts, &mut encoded);
761 let n = encoded.len();
762
763 let mut rlen = self.restarts.len();
765 let mut is_restart = is_restart;
766 if rlen >= MAX_RESTARTS {
767 is_restart = false;
768 }
769 if is_restart {
770 rlen += 1;
771 }
772 if self.block_size > 0 && 2 + 3 * rlen + n > self.block_size - self.next() {
773 return Ok(false);
774 }
775
776 if is_restart {
777 self.restarts.push(self.next() as u32);
778 }
779 self.buf.extend_from_slice(&encoded);
780 self.last_key = key;
781 self.entries += 1;
782 Ok(true)
783 }
784
785 fn finish(&mut self) -> Result<usize> {
789 for &r in &self.restarts {
790 self.buf.push(((r >> 16) & 0xff) as u8);
791 self.buf.push(((r >> 8) & 0xff) as u8);
792 self.buf.push((r & 0xff) as u8);
793 }
794 let rc = self.restarts.len() as u16;
795 self.buf.push((rc >> 8) as u8);
796 self.buf.push((rc & 0xff) as u8);
797
798 let block_len = self.buf.len();
800 self.buf[self.header_off + 1] = ((block_len >> 16) & 0xff) as u8;
801 self.buf[self.header_off + 2] = ((block_len >> 8) & 0xff) as u8;
802 self.buf[self.header_off + 3] = (block_len & 0xff) as u8;
803
804 if self.typ == BLOCK_TYPE_LOG {
805 use flate2::write::ZlibEncoder;
806 use flate2::Compression;
807 let skip = 4 + self.header_off;
808 let mut enc = ZlibEncoder::new(Vec::new(), Compression::new(9));
809 enc.write_all(&self.buf[skip..])
810 .map_err(|e| Error::Zlib(e.to_string()))?;
811 let compressed = enc.finish().map_err(|e| Error::Zlib(e.to_string()))?;
812 self.buf.truncate(skip);
813 self.buf.extend_from_slice(&compressed);
814 }
815 Ok(self.buf.len())
816 }
817}
818
819#[derive(Default, Clone)]
821struct SectionStats {
822 blocks: usize,
823 index_blocks: usize,
824 offset: u64,
825 index_offset: u64,
826}
827
828struct ObjEntry {
830 hash: Vec<u8>,
831 offsets: Vec<u64>,
832}
833
834struct WriterState {
836 opts: WriteOptions,
837 min_update_index: u64,
838 max_update_index: u64,
839
840 out: Vec<u8>,
841 next: u64,
842 pending_padding: usize,
843
844 block: Option<BlockWriter>,
845 block_type: u8,
846
847 index: Vec<(Vec<u8>, u64)>,
849
850 obj_entries: Vec<ObjEntry>,
852 object_id_len: usize,
853
854 ref_stats: SectionStats,
855 obj_stats: SectionStats,
856 log_stats: SectionStats,
857 idx_blocks_total: usize,
858}
859
860impl WriterState {
861 fn new(mut opts: WriteOptions, min: u64, max: u64) -> Self {
862 if opts.restart_interval == 0 {
863 opts.restart_interval = RESTART_INTERVAL;
864 }
865 if opts.block_size == 0 {
866 opts.block_size = REFTABLE_DEFAULT_BLOCK_SIZE;
867 }
868 Self {
869 opts,
870 min_update_index: min,
871 max_update_index: max,
872 out: Vec::new(),
873 next: 0,
874 pending_padding: 0,
875 block: None,
876 block_type: 0,
877 index: Vec::new(),
878 obj_entries: Vec::new(),
879 object_id_len: 0,
880 ref_stats: SectionStats::default(),
881 obj_stats: SectionStats::default(),
882 log_stats: SectionStats::default(),
883 idx_blocks_total: 0,
884 }
885 }
886
887 fn header_size(&self) -> usize {
888 if self.opts.hash_size == 32 {
891 28
892 } else {
893 24
894 }
895 }
896
897 fn write_header(&self, dest: &mut [u8]) {
898 dest[0..4].copy_from_slice(REFTABLE_MAGIC);
899 dest[4] = if self.opts.hash_size == 32 { 2 } else { 1 };
900 dest[5] = ((self.opts.block_size >> 16) & 0xff) as u8;
901 dest[6] = ((self.opts.block_size >> 8) & 0xff) as u8;
902 dest[7] = (self.opts.block_size & 0xff) as u8;
903 dest[8..16].copy_from_slice(&self.min_update_index.to_be_bytes());
904 dest[16..24].copy_from_slice(&self.max_update_index.to_be_bytes());
905 if self.opts.hash_size == 32 {
908 dest[24..28].copy_from_slice(b"s256");
909 }
910 }
911
912 fn stats_mut(&mut self, typ: u8) -> &mut SectionStats {
913 match typ {
914 BLOCK_TYPE_REF => &mut self.ref_stats,
915 BLOCK_TYPE_OBJ => &mut self.obj_stats,
916 BLOCK_TYPE_LOG => &mut self.log_stats,
917 _ => &mut self.ref_stats,
919 }
920 }
921
922 fn padded_write(&mut self, data: &[u8], padding: usize) {
925 if self.pending_padding > 0 {
926 self.out
927 .extend(std::iter::repeat_n(0u8, self.pending_padding));
928 self.pending_padding = 0;
929 }
930 self.pending_padding = padding;
931 self.out.extend_from_slice(data);
932 }
933
934 fn reinit_block(&mut self, typ: u8) {
935 let header_off = if self.next == 0 {
936 self.header_size()
937 } else {
938 0
939 };
940 self.block = Some(BlockWriter::new(
941 typ,
942 self.opts.block_size as usize,
943 header_off,
944 self.opts.restart_interval,
945 ));
946 self.block_type = typ;
947 }
948
949 fn add_record(&mut self, rec: &EncRecord) -> Result<()> {
950 let typ = rec.block_type();
951 if self.block.is_none() {
952 self.reinit_block(typ);
953 }
954 if self.add_to_block(rec)? {
955 return Ok(());
956 }
957 self.flush_block()?;
959 self.reinit_block(typ);
960 if !self.add_to_block(rec)? {
961 return Err(ReftableError::EntryTooLarge.into());
962 }
963 Ok(())
964 }
965
966 fn add_to_block(&mut self, rec: &EncRecord) -> Result<bool> {
969 match self.block.as_mut() {
970 Some(bw) => bw.add(rec, &self.opts),
971 None => Ok(false),
972 }
973 }
974
975 fn add_ref(&mut self, r: &RefRecord) -> Result<()> {
976 let delta = r.update_index.saturating_sub(self.min_update_index);
977 self.add_record(&EncRecord::Ref(r, delta))?;
978
979 if !self.opts.skip_index_objects {
980 match &r.value {
981 RefValue::Val1(oid) => self.index_hash(oid.as_bytes()),
982 RefValue::Val2(oid, peeled) => {
983 self.index_hash(oid.as_bytes());
984 self.index_hash(peeled.as_bytes());
985 }
986 _ => {}
987 }
988 }
989 Ok(())
990 }
991
992 fn add_log(&mut self, l: &LogRecord) -> Result<()> {
993 if matches!(&self.block, Some(b) if b.typ == BLOCK_TYPE_REF) {
995 self.finish_public_section()?;
996 }
997 self.next -= self.pending_padding as u64;
999 self.pending_padding = 0;
1000 self.add_record(&EncRecord::Log(l))
1001 }
1002
1003 fn index_hash(&mut self, hash: &[u8]) {
1004 let off = self.next;
1005 match self
1006 .obj_entries
1007 .binary_search_by(|e| e.hash.as_slice().cmp(hash))
1008 {
1009 Ok(idx) => {
1010 let e = &mut self.obj_entries[idx];
1011 if e.offsets.last() != Some(&off) {
1012 e.offsets.push(off);
1013 }
1014 }
1015 Err(idx) => {
1016 self.obj_entries.insert(
1017 idx,
1018 ObjEntry {
1019 hash: hash.to_vec(),
1020 offsets: vec![off],
1021 },
1022 );
1023 }
1024 }
1025 }
1026
1027 fn flush_block(&mut self) -> Result<()> {
1029 let Some(mut bw) = self.block.take() else {
1030 return Ok(());
1031 };
1032 if bw.entries == 0 {
1033 self.block = Some(bw);
1034 return Ok(());
1035 }
1036 let typ = bw.typ;
1037 let raw_bytes = bw.finish()?;
1038
1039 let mut padding = 0;
1040 if !self.opts.unpadded && typ != BLOCK_TYPE_LOG {
1041 padding = (self.opts.block_size as usize).saturating_sub(raw_bytes);
1042 }
1043
1044 let block_typ_off = if self.stats_mut(typ).blocks == 0 {
1045 self.next
1046 } else {
1047 0
1048 };
1049 {
1050 let next = self.next;
1051 let st = self.stats_mut(typ);
1052 if block_typ_off > 0 {
1053 st.offset = next;
1054 }
1055 st.blocks += 1;
1056 }
1057
1058 if self.next == 0 {
1059 let hs = self.header_size();
1061 self.write_header_into_block(&mut bw, hs);
1062 }
1063
1064 let data = bw.buf.clone();
1065 self.padded_write(&data, padding);
1066
1067 self.index.push((bw.last_key.clone(), self.next));
1069
1070 self.next += (padding + raw_bytes) as u64;
1071 self.block = None;
1072 Ok(())
1073 }
1074
1075 fn write_header_into_block(&self, bw: &mut BlockWriter, hs: usize) {
1076 let mut hdr = vec![0u8; hs];
1077 self.write_header(&mut hdr);
1078 bw.buf[..hs].copy_from_slice(&hdr);
1079 }
1080
1081 fn flush_block_if_nonempty(&mut self) -> Result<()> {
1082 if matches!(&self.block, Some(b) if b.entries == 0) {
1083 return Ok(());
1084 }
1085 self.flush_block()
1086 }
1087
1088 fn finish_section(&mut self) -> Result<()> {
1090 let typ = self.block_type;
1091 let threshold = if self.opts.unpadded { 1 } else { 3 };
1092 let before_blocks = self.idx_blocks_total;
1093
1094 self.flush_block_if_nonempty()?;
1095
1096 let mut max_level = 0;
1097 let mut index_start = 0u64;
1098
1099 while self.index.len() > threshold {
1100 max_level += 1;
1101 index_start = self.next;
1102 self.reinit_block(BLOCK_TYPE_INDEX);
1103
1104 let idx = std::mem::take(&mut self.index);
1105 for (last_key, offset) in &idx {
1106 self.add_record(&EncRecord::Index {
1107 last_key: last_key.clone(),
1108 offset: *offset,
1109 })?;
1110 }
1111 let blocks_before = self.count_index_blocks_marker();
1113 self.flush_index_block()?;
1114 let _ = blocks_before;
1115 }
1116
1117 self.index.clear();
1118
1119 let index_blocks = self.idx_blocks_total - before_blocks;
1120 {
1121 let st = self.stats_mut(typ);
1122 st.index_blocks = index_blocks;
1123 st.index_offset = index_start;
1124 }
1125 let _ = max_level;
1126 Ok(())
1127 }
1128
1129 fn count_index_blocks_marker(&self) -> usize {
1130 self.idx_blocks_total
1131 }
1132
1133 fn flush_index_block(&mut self) -> Result<()> {
1137 let Some(mut bw) = self.block.take() else {
1138 return Ok(());
1139 };
1140 if bw.entries == 0 {
1141 self.block = Some(bw);
1142 return Ok(());
1143 }
1144 let raw_bytes = bw.finish()?;
1145 let mut padding = 0;
1146 if !self.opts.unpadded {
1147 padding = (self.opts.block_size as usize).saturating_sub(raw_bytes);
1148 }
1149 if self.next == 0 {
1150 let hs = self.header_size();
1151 self.write_header_into_block(&mut bw, hs);
1152 }
1153 let data = bw.buf.clone();
1154 self.padded_write(&data, padding);
1155 self.index.push((bw.last_key.clone(), self.next));
1156 self.next += (padding + raw_bytes) as u64;
1157 self.idx_blocks_total += 1;
1158 self.block = None;
1159 Ok(())
1160 }
1161
1162 fn dump_object_index(&mut self) -> Result<()> {
1164 let mut max_common = 1usize;
1166 for w in self.obj_entries.windows(2) {
1167 let n = common_prefix_len(&w[0].hash, &w[1].hash);
1168 if n > max_common {
1169 max_common = n;
1170 }
1171 }
1172 self.object_id_len = max_common + 1;
1173 let id_len = self.object_id_len;
1174
1175 self.reinit_block(BLOCK_TYPE_OBJ);
1176 let entries = std::mem::take(&mut self.obj_entries);
1177 for e in &entries {
1178 let prefix = e.hash[..id_len.min(e.hash.len())].to_vec();
1179 self.add_obj_record(prefix, &e.offsets)?;
1180 }
1181 self.obj_entries = entries;
1182 self.finish_section()
1183 }
1184
1185 fn add_obj_record(&mut self, prefix: Vec<u8>, offsets: &[u64]) -> Result<()> {
1186 let typ = BLOCK_TYPE_OBJ;
1188 if self.block.is_none() {
1189 self.reinit_block(typ);
1190 }
1191 let rec = EncRecord::Obj {
1192 prefix: prefix.clone(),
1193 offsets: offsets.to_vec(),
1194 };
1195 if self.add_to_block(&rec)? {
1196 return Ok(());
1197 }
1198 self.flush_block()?;
1199 self.reinit_block(typ);
1200 if self.add_to_block(&rec)? {
1201 return Ok(());
1202 }
1203 let rec = EncRecord::Obj {
1205 prefix,
1206 offsets: Vec::new(),
1207 };
1208 self.add_to_block(&rec)?;
1209 Ok(())
1210 }
1211
1212 fn finish_public_section(&mut self) -> Result<()> {
1214 let Some(bw) = &self.block else {
1215 return Ok(());
1216 };
1217 let typ = bw.typ;
1218 self.finish_section()?;
1219 if typ == BLOCK_TYPE_REF && !self.opts.skip_index_objects && self.ref_stats.index_blocks > 0
1220 {
1221 self.dump_object_index()?;
1222 }
1223 self.obj_entries.clear();
1224 self.block = None;
1225 self.block_type = 0;
1226 Ok(())
1227 }
1228
1229 fn close(mut self) -> Result<Vec<u8>> {
1231 self.finish_public_section()?;
1232 let empty_table = self.next == 0;
1233 self.pending_padding = 0;
1234
1235 if empty_table {
1236 let hs = self.header_size();
1237 let mut header = vec![0u8; hs];
1238 self.write_header(&mut header);
1239 self.padded_write(&header, 0);
1240 }
1241
1242 let mut footer = vec![0u8; self.header_size()];
1243 self.write_header(&mut footer);
1244 footer.extend_from_slice(&self.ref_stats.index_offset.to_be_bytes());
1245 let obj_field = (self.obj_stats.offset << 5) | (self.object_id_len as u64);
1246 footer.extend_from_slice(&obj_field.to_be_bytes());
1247 footer.extend_from_slice(&self.obj_stats.index_offset.to_be_bytes());
1248 footer.extend_from_slice(&self.log_stats.offset.to_be_bytes());
1249 footer.extend_from_slice(&self.log_stats.index_offset.to_be_bytes());
1250 let crc = crc32(&footer);
1251 footer.extend_from_slice(&crc.to_be_bytes());
1252
1253 self.pending_padding = 0;
1255 self.out.extend_from_slice(&footer);
1256
1257 Ok(self.out)
1258 }
1259}
1260
1261enum TableBytes {
1267 Owned(Arc<[u8]>),
1268 Mapped(Arc<Mmap>),
1269}
1270
1271impl TableBytes {
1272 fn as_slice(&self) -> &[u8] {
1273 match self {
1274 TableBytes::Owned(data) => data.as_ref(),
1275 TableBytes::Mapped(map) => map.as_ref(),
1276 }
1277 }
1278
1279 #[cfg(test)]
1280 fn is_mapped(&self) -> bool {
1281 matches!(self, TableBytes::Mapped(_))
1282 }
1283}
1284
1285pub struct ReftableReader {
1287 data: TableBytes,
1288 version: u8,
1289 block_size: u32,
1290 min_update_index: u64,
1291 max_update_index: u64,
1292 ref_index_position: u64,
1293 log_position: u64,
1294}
1295
1296#[derive(Debug)]
1298struct Footer {
1299 block_size: u32,
1300 min_update_index: u64,
1301 max_update_index: u64,
1302 ref_index_position: u64,
1303 log_position: u64,
1304}
1305
1306impl ReftableReader {
1307 fn bytes(&self) -> &[u8] {
1308 self.data.as_slice()
1309 }
1310
1311 pub fn new(data: Vec<u8>) -> Result<Self> {
1313 Self::from_arc(Arc::from(data))
1314 }
1315
1316 pub fn from_arc(data: Arc<[u8]>) -> Result<Self> {
1318 Self::open(TableBytes::Owned(data))
1319 }
1320
1321 pub fn from_mapped(map: Arc<Mmap>) -> Result<Self> {
1323 Self::open(TableBytes::Mapped(map))
1324 }
1325
1326 #[cfg(test)]
1328 pub(crate) fn table_backing_is_mapped(&self) -> bool {
1329 self.data.is_mapped()
1330 }
1331
1332 fn open(data: TableBytes) -> Result<Self> {
1333 let bytes = data.as_slice();
1334 if bytes.len() < HEADER_SIZE + FOOTER_V1_SIZE {
1335 if bytes.len() < HEADER_SIZE {
1337 return Err(ReftableError::TooSmall { len: bytes.len() }.into());
1338 }
1339 }
1340
1341 if &bytes[0..4] != REFTABLE_MAGIC {
1343 return Err(ReftableError::BadMagic.into());
1344 }
1345 let version = bytes[4];
1346 if version != 1 && version != 2 {
1347 return Err(ReftableError::UnsupportedVersion { version }.into());
1348 }
1349 let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
1351 if bytes.len() < footer_size {
1352 return Err(ReftableError::TooSmall { len: bytes.len() }.into());
1353 }
1354 let footer_start = bytes.len() - footer_size;
1355 let footer = parse_footer(&bytes[footer_start..], version)?;
1356
1357 Ok(Self {
1358 data,
1359 version,
1360 block_size: footer.block_size,
1361 min_update_index: footer.min_update_index,
1362 max_update_index: footer.max_update_index,
1363 ref_index_position: footer.ref_index_position,
1364 log_position: footer.log_position,
1365 })
1366 }
1367
1368 fn hash_size(&self) -> usize {
1371 if self.version == 2 {
1372 32
1373 } else {
1374 20
1375 }
1376 }
1377
1378 fn header_len(&self) -> usize {
1380 if self.version == 2 {
1381 28
1382 } else {
1383 HEADER_SIZE
1384 }
1385 }
1386
1387 pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
1389 let mut refs = Vec::new();
1390 self.for_each_ref(|rec| {
1391 refs.push(rec.clone());
1392 ControlFlow::<()>::Continue(())
1393 })?;
1394 Ok(refs)
1395 }
1396
1397 pub fn for_each_ref(&self, f: impl FnMut(&RefRecord) -> ControlFlow<()>) -> Result<()> {
1401 self.for_each_ref_in_range("", None, f)
1402 }
1403
1404 pub fn for_each_ref_in_range(
1406 &self,
1407 lower: &str,
1408 upper_exclusive: Option<&str>,
1409 mut f: impl FnMut(&RefRecord) -> ControlFlow<()>,
1410 ) -> Result<()> {
1411 let footer_size = if self.version == 2 {
1412 72
1413 } else {
1414 FOOTER_V1_SIZE
1415 };
1416 let file_end = self.bytes().len() - footer_size;
1417
1418 let ref_end = if self.ref_index_position > 0 {
1419 self.ref_index_position as usize
1420 } else if self.log_position > 0 {
1421 self.log_position as usize
1422 } else {
1423 file_end
1424 };
1425
1426 let header_len = self.header_len();
1427 let mut pos = if self.ref_index_position > 0 && !lower.is_empty() {
1428 self.locate_ref_block_via_index(lower.as_bytes())?
1429 .unwrap_or(header_len)
1430 } else {
1431 header_len
1432 };
1433
1434 while pos < ref_end {
1435 if pos >= self.bytes().len() {
1436 break;
1437 }
1438 let block_type = self.bytes()[pos];
1439 if block_type == 0 {
1440 if self.block_size > 0 {
1441 let bs = self.block_size as usize;
1442 pos = ((pos / bs) + 1) * bs;
1443 continue;
1444 }
1445 break;
1446 }
1447 if block_type != BLOCK_TYPE_REF {
1448 break;
1449 }
1450
1451 let block_len = read_u24(self.bytes(), pos + 1);
1452 let block_data_start = pos + 4;
1453 let is_first = pos == header_len;
1454 let records_end = if is_first { block_len } else { pos + block_len };
1455
1456 if records_end > ref_end {
1457 break;
1458 }
1459
1460 let restart_table_start =
1461 restart_table_start_for_block(self.bytes(), records_end, block_data_start)?;
1462
1463 let mut rpos = block_data_start;
1464 let mut prev_name = Vec::<u8>::new();
1465
1466 while rpos < restart_table_start {
1467 let (rec, new_pos) = decode_ref_record(
1468 self.bytes(),
1469 rpos,
1470 &prev_name,
1471 self.min_update_index,
1472 self.hash_size(),
1473 )?;
1474 prev_name = rec.name.as_bytes().to_vec();
1475 if rec.name.as_str() < lower {
1476 rpos = new_pos;
1477 continue;
1478 }
1479 if let Some(upper) = upper_exclusive {
1480 if rec.name.as_str() >= upper {
1481 return Ok(());
1482 }
1483 }
1484 if f(&rec).is_break() {
1485 return Ok(());
1486 }
1487 rpos = new_pos;
1488 }
1489
1490 if self.block_size > 0 {
1491 let bs = self.block_size as usize;
1492 pos = if is_first { bs } else { pos + bs };
1493 } else {
1494 pos = records_end;
1495 }
1496 }
1497
1498 Ok(())
1499 }
1500
1501 pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
1503 let mut found = None;
1504 self.for_each_ref_in_range(name, None, |rec| {
1505 if rec.name == name {
1506 found = Some(rec.clone());
1507 return ControlFlow::Break(());
1508 }
1509 if rec.name.as_str() > name {
1510 return ControlFlow::Break(());
1511 }
1512 ControlFlow::Continue(())
1513 })?;
1514 Ok(found)
1515 }
1516
1517 fn locate_ref_block_via_index(&self, name: &[u8]) -> Result<Option<usize>> {
1518 let mut pos = self.ref_index_position as usize;
1519 if pos == 0 {
1520 return Ok(None);
1521 }
1522 for _ in 0..64 {
1523 if pos >= self.bytes().len() {
1524 return Ok(None);
1525 }
1526 match self.bytes()[pos] {
1527 BLOCK_TYPE_REF => return Ok(Some(pos)),
1528 BLOCK_TYPE_INDEX => {
1529 let Some(offset) = self.index_block_seek_offset(pos, name)? else {
1530 return Ok(None);
1531 };
1532 let next = offset as usize;
1533 if next == pos {
1534 return Ok(None);
1535 }
1536 pos = next;
1537 }
1538 _ => return Ok(None),
1539 }
1540 }
1541 Ok(None)
1542 }
1543
1544 fn index_block_seek_offset(&self, block_pos: usize, name: &[u8]) -> Result<Option<u64>> {
1547 let (records_start, records_end) = self.block_record_range(block_pos)?;
1548 let mut rpos = records_start;
1549 let mut prev_key = Vec::<u8>::new();
1550 let mut last: Option<u64> = None;
1551 while rpos < records_end {
1552 let (key, offset, new_pos) = decode_index_record(self.bytes(), rpos, &prev_key)?;
1553 prev_key = key;
1554 if prev_key.as_slice() >= name {
1555 return Ok(Some(offset));
1556 }
1557 last = Some(offset);
1558 rpos = new_pos;
1559 }
1560 Ok(last)
1561 }
1562
1563 fn block_record_range(&self, block_pos: usize) -> Result<(usize, usize)> {
1564 if block_pos + 4 > self.bytes().len() {
1565 return Err(truncated(ReftableField::BlockHeader));
1566 }
1567 let block_len = read_u24(self.bytes(), block_pos + 1);
1568 let header_len = self.header_len();
1569 let is_first = block_pos == header_len;
1570 let records_end = if is_first {
1571 block_len
1572 } else {
1573 block_pos + block_len
1574 };
1575 if records_end > self.bytes().len() || records_end < 2 {
1576 return Err(corrupt(ReftableError::InvalidBlockLength));
1577 }
1578 let records_data_start = block_pos + 4;
1579 let restart_table_start =
1580 restart_table_start_for_block(self.bytes(), records_end, records_data_start)?;
1581 Ok((records_data_start, restart_table_start))
1582 }
1583
1584 pub fn read_logs(&self) -> Result<Vec<LogRecord>> {
1586 let footer_size = if self.version == 2 {
1587 72
1588 } else {
1589 FOOTER_V1_SIZE
1590 };
1591 let file_end = self.bytes().len() - footer_size;
1592
1593 let mut pos = if self.log_position > 0 {
1601 self.log_position as usize
1602 } else if self.bytes().len() > self.header_len()
1603 && self.bytes()[self.header_len()] == BLOCK_TYPE_LOG
1604 {
1605 self.header_len()
1607 } else {
1608 return Ok(Vec::new());
1609 };
1610 let mut logs = Vec::new();
1611
1612 while pos < file_end {
1613 if pos >= self.bytes().len() {
1614 break;
1615 }
1616 let block_type = self.bytes()[pos];
1617 if block_type != BLOCK_TYPE_LOG {
1618 break;
1619 }
1620 let is_first = pos == self.header_len() && self.log_position == 0;
1625 let block_len = read_u24(self.bytes(), pos + 1);
1626 let compressed_start = pos + 4;
1627
1628 let header_prefix = if is_first { self.header_len() } else { 0 };
1631 let inflated_size = block_len.saturating_sub(4 + header_prefix);
1632
1633 let remaining = &self.bytes()[compressed_start..file_end];
1634 let (inflated, consumed) = decompress_log_payload(remaining, inflated_size)?;
1635
1636 if inflated.len() < 2 {
1639 break;
1640 }
1641 let records_end = inflated.len();
1642 let restart_table_start = restart_table_start_for_block(&inflated, records_end, 0)?;
1643
1644 let mut rpos = 0usize;
1645 let mut prev_key = Vec::<u8>::new();
1646
1647 while rpos < restart_table_start {
1648 let (log, new_pos) =
1649 decode_log_record(&inflated, rpos, &prev_key, self.hash_size())?;
1650 let mut key = Vec::new();
1652 key.extend_from_slice(log.refname.as_bytes());
1653 key.push(0);
1654 key.extend_from_slice(&(0xffffffffffffffffu64 - log.update_index).to_be_bytes());
1655 prev_key = key;
1656 logs.push(log);
1657 rpos = new_pos;
1658 }
1659
1660 pos = compressed_start + consumed;
1661 }
1662
1663 Ok(logs)
1664 }
1665
1666 pub fn block_size(&self) -> u32 {
1668 self.block_size
1669 }
1670
1671 pub fn min_update_index(&self) -> u64 {
1673 self.min_update_index
1674 }
1675
1676 pub fn max_update_index(&self) -> u64 {
1678 self.max_update_index
1679 }
1680
1681 pub fn ref_index_offset(&self) -> u64 {
1683 self.ref_index_position
1684 }
1685
1686 pub fn payload_size(&self) -> u64 {
1688 let overhead = if self.version == 2 { 28 } else { 24 };
1689 self.bytes().len().saturating_sub(overhead) as u64
1690 }
1691}
1692
1693fn decode_index_record(data: &[u8], pos: usize, prev_key: &[u8]) -> Result<(Vec<u8>, u64, usize)> {
1698 let (prefix_len, p) = get_varint(data, pos)?;
1699 let (suffix_and_type, mut p) = get_varint(data, p)?;
1700 let suffix_len = (suffix_and_type >> 3) as usize;
1701
1702 let mut key = Vec::with_capacity(prefix_len as usize + suffix_len);
1703 if prefix_len > 0 {
1704 if (prefix_len as usize) > prev_key.len() {
1705 return Err(corrupt(ReftableError::PrefixTooLong {
1706 field: ReftableField::IndexKey,
1707 }));
1708 }
1709 key.extend_from_slice(&prev_key[..prefix_len as usize]);
1710 }
1711 if p + suffix_len > data.len() {
1712 return Err(truncated(ReftableField::IndexKey));
1713 }
1714 key.extend_from_slice(&data[p..p + suffix_len]);
1715 p += suffix_len;
1716
1717 let (offset, p) = get_varint(data, p)?;
1718 Ok((key, offset, p))
1719}
1720
1721fn decode_ref_record(
1722 data: &[u8],
1723 pos: usize,
1724 prev_name: &[u8],
1725 min_update_index: u64,
1726 hash_size: usize,
1727) -> Result<(RefRecord, usize)> {
1728 let (prefix_len, p) = get_varint(data, pos)?;
1729 let (suffix_and_type, mut p) = get_varint(data, p)?;
1730 let suffix_len = (suffix_and_type >> 3) as usize;
1731 let value_type = (suffix_and_type & 0x7) as u8;
1732
1733 let mut name = Vec::with_capacity(prefix_len as usize + suffix_len);
1735 if prefix_len > 0 {
1736 if (prefix_len as usize) > prev_name.len() {
1737 return Err(corrupt(ReftableError::PrefixTooLong {
1738 field: ReftableField::RefName,
1739 }));
1740 }
1741 name.extend_from_slice(&prev_name[..prefix_len as usize]);
1742 }
1743 if p + suffix_len > data.len() {
1744 return Err(truncated(ReftableField::RefName));
1745 }
1746 name.extend_from_slice(&data[p..p + suffix_len]);
1747 p += suffix_len;
1748
1749 let name_str = String::from_utf8(name).map_err(|_| invalid_utf8(ReftableField::RefName))?;
1750
1751 let (update_index_delta, mut p) = get_varint(data, p)?;
1752 let update_index = min_update_index + update_index_delta;
1753
1754 let value = match value_type {
1755 VALUE_DELETION => RefValue::Deletion,
1756 VALUE_ONE_OID => {
1757 if p + hash_size > data.len() {
1758 return Err(truncated(ReftableField::ObjectId));
1759 }
1760 let oid = ObjectId::from_bytes(&data[p..p + hash_size])?;
1761 p += hash_size;
1762 RefValue::Val1(oid)
1763 }
1764 VALUE_TWO_OID => {
1765 if p + 2 * hash_size > data.len() {
1766 return Err(truncated(ReftableField::ObjectIdPair));
1767 }
1768 let oid = ObjectId::from_bytes(&data[p..p + hash_size])?;
1769 p += hash_size;
1770 let peeled = ObjectId::from_bytes(&data[p..p + hash_size])?;
1771 p += hash_size;
1772 RefValue::Val2(oid, peeled)
1773 }
1774 VALUE_SYMREF => {
1775 let (target_len, p2) = get_varint(data, p)?;
1776 p = p2;
1777 let target_len = target_len as usize;
1778 if p + target_len > data.len() {
1779 return Err(truncated(ReftableField::SymrefTarget));
1780 }
1781 let target = String::from_utf8(data[p..p + target_len].to_vec())
1782 .map_err(|_| invalid_utf8(ReftableField::SymrefTarget))?;
1783 p += target_len;
1784 RefValue::Symref(target)
1785 }
1786 _ => {
1787 return Err(corrupt(ReftableError::UnknownValueType { value_type }));
1788 }
1789 };
1790
1791 Ok((
1792 RefRecord {
1793 name: name_str,
1794 update_index,
1795 value,
1796 },
1797 p,
1798 ))
1799}
1800
1801fn decode_log_record(
1802 data: &[u8],
1803 pos: usize,
1804 prev_key: &[u8],
1805 hash_size: usize,
1806) -> Result<(LogRecord, usize)> {
1807 let (prefix_len, p) = get_varint(data, pos)?;
1808 let (suffix_and_type, mut p) = get_varint(data, p)?;
1809 let suffix_len = (suffix_and_type >> 3) as usize;
1810 let log_type = (suffix_and_type & 0x7) as u8;
1811
1812 let mut key = Vec::with_capacity(prefix_len as usize + suffix_len);
1814 if prefix_len > 0 {
1815 if (prefix_len as usize) > prev_key.len() {
1816 return Err(corrupt(ReftableError::PrefixTooLong {
1817 field: ReftableField::LogKey,
1818 }));
1819 }
1820 key.extend_from_slice(&prev_key[..prefix_len as usize]);
1821 }
1822 if p + suffix_len > data.len() {
1823 return Err(truncated(ReftableField::LogKey));
1824 }
1825 key.extend_from_slice(&data[p..p + suffix_len]);
1826 p += suffix_len;
1827
1828 let null_pos = key
1830 .iter()
1831 .position(|&b| b == 0)
1832 .ok_or_else(|| corrupt(ReftableError::InvalidLogKey))?;
1833 let refname = String::from_utf8(key[..null_pos].to_vec())
1834 .map_err(|_| invalid_utf8(ReftableField::RefName))?;
1835 if null_pos + 9 > key.len() {
1836 return Err(corrupt(ReftableError::InvalidLogKey));
1837 }
1838 let reversed_idx = u64::from_be_bytes(
1839 key[null_pos + 1..null_pos + 9]
1840 .try_into()
1841 .map_err(|_| corrupt(ReftableError::InvalidLogKey))?,
1842 );
1843 let update_index = 0xffffffffffffffffu64 - reversed_idx;
1844
1845 if log_type == 0 {
1846 let zero_oid = ObjectId::from_bytes(&vec![0u8; hash_size])?;
1848 return Ok((
1849 LogRecord {
1850 refname,
1851 update_index,
1852 old_id: zero_oid,
1853 new_id: zero_oid,
1854 name: String::new(),
1855 email: String::new(),
1856 time_seconds: 0,
1857 tz_offset: 0,
1858 message: String::new(),
1859 },
1860 p,
1861 ));
1862 }
1863
1864 if p + 2 * hash_size > data.len() {
1866 return Err(truncated(ReftableField::ObjectIdPair));
1867 }
1868 let old_id = ObjectId::from_bytes(&data[p..p + hash_size])?;
1869 p += hash_size;
1870 let new_id = ObjectId::from_bytes(&data[p..p + hash_size])?;
1871 p += hash_size;
1872
1873 let (name_len, p2) = get_varint(data, p)?;
1874 p = p2;
1875 let name_len = name_len as usize;
1876 if p + name_len > data.len() {
1877 return Err(truncated(ReftableField::LogName));
1878 }
1879 let name = String::from_utf8(data[p..p + name_len].to_vec())
1880 .map_err(|_| invalid_utf8(ReftableField::LogName))?;
1881 p += name_len;
1882
1883 let (email_len, p2) = get_varint(data, p)?;
1884 p = p2;
1885 let email_len = email_len as usize;
1886 if p + email_len > data.len() {
1887 return Err(truncated(ReftableField::LogEmail));
1888 }
1889 let email = String::from_utf8(data[p..p + email_len].to_vec())
1890 .map_err(|_| invalid_utf8(ReftableField::LogEmail))?;
1891 p += email_len;
1892
1893 let (time_seconds, p2) = get_varint(data, p)?;
1894 p = p2;
1895
1896 if p + 2 > data.len() {
1897 return Err(truncated(ReftableField::LogTimezone));
1898 }
1899 let tz_offset = i16::from_be_bytes([data[p], data[p + 1]]);
1900 p += 2;
1901
1902 let (msg_len, p2) = get_varint(data, p)?;
1903 p = p2;
1904 let msg_len = msg_len as usize;
1905 if p + msg_len > data.len() {
1906 return Err(truncated(ReftableField::LogMessage));
1907 }
1908 let message = String::from_utf8(data[p..p + msg_len].to_vec())
1909 .map_err(|_| invalid_utf8(ReftableField::LogMessage))?;
1910 p += msg_len;
1911
1912 Ok((
1913 LogRecord {
1914 refname,
1915 update_index,
1916 old_id,
1917 new_id,
1918 name,
1919 email,
1920 time_seconds,
1921 tz_offset,
1922 message,
1923 },
1924 p,
1925 ))
1926}
1927
1928pub(crate) fn widen_oid_to(oid: ObjectId, hash_size: usize) -> ObjectId {
1936 if oid.is_zero() && oid.as_bytes().len() != hash_size {
1937 ObjectId::from_bytes(&vec![0u8; hash_size]).unwrap_or(oid)
1938 } else {
1939 oid
1940 }
1941}
1942
1943fn reftable_hash_size_for_git_dir(git_dir: &Path) -> usize {
1946 let cfg = crate::config::ConfigSet::load(
1947 &crate::environment::Environment::empty(),
1948 Some(git_dir),
1949 true,
1950 )
1951 .unwrap_or_default();
1952 match cfg
1953 .get("extensions.objectformat")
1954 .and_then(|v| crate::objects::HashAlgo::from_name(&v))
1955 {
1956 Some(crate::objects::HashAlgo::Sha256) => 32,
1957 _ => 20,
1958 }
1959}
1960
1961fn prefix_upper_exclusive(prefix: &str) -> Option<String> {
1964 if prefix.is_empty() || prefix.ends_with('/') {
1965 return None;
1966 }
1967 let mut end = prefix.as_bytes().to_vec();
1968 while let Some(last) = end.last_mut() {
1969 if *last == b'/' {
1970 break;
1971 }
1972 if *last < 0xff {
1973 *last += 1;
1974 return String::from_utf8(end).ok();
1975 }
1976 *last = 0;
1977 end.pop();
1978 }
1979 None
1980}
1981
1982fn suggest_compaction_segment(sizes: &[u64], geometric_factor: u8) -> (usize, usize) {
1984 let n = sizes.len();
1985 if n <= 1 {
1986 return (0, 0);
1987 }
1988 let factor = u64::from(geometric_factor.max(2));
1989 let mut seg_end = 0usize;
1990 let mut bytes = 0u64;
1991 let mut i = n - 1;
1992 while i > 0 {
1993 if sizes[i - 1] < sizes[i].saturating_mul(factor) {
1994 seg_end = i + 1;
1995 bytes = sizes[i];
1996 break;
1997 }
1998 i -= 1;
1999 }
2000 if seg_end == 0 {
2001 return (0, 0);
2002 }
2003 let mut seg_start = 0usize;
2004 i = seg_end.saturating_sub(1);
2005 while i > 0 {
2006 let curr = bytes;
2007 bytes = bytes.saturating_add(sizes[i - 1]);
2008 if sizes[i - 1] < curr.saturating_mul(factor) {
2009 seg_start = i - 1;
2010 }
2011 i -= 1;
2012 }
2013 if seg_end <= seg_start + 1 {
2014 return (0, 0);
2015 }
2016 (seg_start, seg_end)
2017}
2018
2019#[derive(Clone, PartialEq, Eq, Hash)]
2020struct StackCacheKey {
2021 reftable_dir: PathBuf,
2022 tables_list_len: u64,
2023 tables_list_modified: i64,
2024}
2025
2026struct CachedStackTables {
2027 table_names: Vec<String>,
2028 readers: Vec<ReftableReader>,
2029}
2030
2031static STACK_TABLE_CACHE: OnceLock<Mutex<HashMap<StackCacheKey, Arc<CachedStackTables>>>> =
2033 OnceLock::new();
2034
2035fn stack_table_cache() -> &'static Mutex<HashMap<StackCacheKey, Arc<CachedStackTables>>> {
2037 STACK_TABLE_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
2038}
2039
2040const STACK_TABLE_RELOAD_DEADLINE: Duration = Duration::from_secs(3);
2042
2043fn is_transient_stack_table_error(err: &Error) -> bool {
2044 match err {
2045 Error::Io(e) if e.kind() == std::io::ErrorKind::NotFound => true,
2046 Error::Reftable(ReftableError::TooSmall { len: 0 }) => true,
2047 _ => false,
2048 }
2049}
2050
2051fn random_table_suffix() -> Result<u32> {
2052 #[cfg(test)]
2053 if let Some(suffix) = reftable_write_test_hooks::take_table_suffix() {
2054 return Ok(suffix);
2055 }
2056 let mut buf = [0u8; 4];
2057 getrandom::getrandom(&mut buf).map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?;
2058 Ok(u32::from_le_bytes(buf))
2059}
2060
2061fn reftable_table_filename(min_update_index: u64, max_update_index: u64, suffix: u32) -> String {
2062 format!(
2063 "{:08x}-{:08x}-{:08x}.ref",
2064 min_update_index, max_update_index, suffix
2065 )
2066}
2067
2068fn publish_new_reftable_table_file(
2070 reftable_dir: &Path,
2071 data: &[u8],
2072 min_update_index: u64,
2073 max_update_index: u64,
2074 suffix: u32,
2075) -> Result<String> {
2076 let filename = reftable_table_filename(min_update_index, max_update_index, suffix);
2077 let path = reftable_dir.join(&filename);
2078 if path.exists() {
2079 return Err(Error::Io(std::io::Error::new(
2080 std::io::ErrorKind::AlreadyExists,
2081 "reftable table file already exists",
2082 )));
2083 }
2084 let tmp_path = reftable_dir.join(format!(".writ-{suffix:08x}.tmp"));
2085 let mut file = match fs::OpenOptions::new()
2086 .write(true)
2087 .create_new(true)
2088 .open(&tmp_path)
2089 {
2090 Ok(file) => file,
2091 Err(err) => return Err(Error::Io(err)),
2092 };
2093 if let Err(err) = file.write_all(data).and_then(|_| file.sync_all()) {
2094 drop(file);
2095 let _ = fs::remove_file(&tmp_path);
2096 return Err(Error::Io(err));
2097 }
2098 drop(file);
2099
2100 #[cfg(test)]
2101 reftable_write_test_hooks::run_before_persist_hook(&path);
2102
2103 let staged = TempPath::try_from_path(tmp_path).map_err(Error::Io)?;
2104 match publish_no_clobber(staged, &path) {
2105 Ok(true) => Ok(filename),
2106 Ok(false) => Err(Error::Io(std::io::Error::new(
2107 std::io::ErrorKind::AlreadyExists,
2108 "reftable table file already exists",
2109 ))),
2110 Err(err) => Err(Error::Io(err)),
2111 }
2112}
2113
2114fn invalidate_stack_table_cache(reftable_dir: &Path) {
2115 let Ok(mut guard) = stack_table_cache().lock() else {
2116 return;
2117 };
2118 guard.retain(|k, _| k.reftable_dir != reftable_dir);
2119}
2120
2121fn tables_list_cache_key(reftable_dir: &Path) -> Result<StackCacheKey> {
2122 let tables_list = reftable_dir.join("tables.list");
2123 match fs::metadata(&tables_list) {
2124 Ok(meta) => Ok(StackCacheKey {
2125 reftable_dir: reftable_dir.to_path_buf(),
2126 tables_list_len: meta.len(),
2127 tables_list_modified: meta
2128 .modified()
2129 .ok()
2130 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
2131 .map(|d| d.as_secs() as i64)
2132 .unwrap_or(0),
2133 }),
2134 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(StackCacheKey {
2135 reftable_dir: reftable_dir.to_path_buf(),
2136 tables_list_len: 0,
2137 tables_list_modified: 0,
2138 }),
2139 Err(e) => Err(Error::Io(e)),
2140 }
2141}
2142
2143fn build_cached_stack_tables(
2144 reftable_dir: &Path,
2145 table_names: &[String],
2146) -> Result<CachedStackTables> {
2147 let mut readers = Vec::with_capacity(table_names.len());
2148 for name in table_names {
2149 let path = reftable_dir.join(name);
2150 let file = fs::File::open(&path).map_err(Error::Io)?;
2151 let len = file.metadata().map_err(Error::Io)?.len() as usize;
2152 let map = Arc::new(crate::pack_map::map_read_only_file(&file, len).map_err(Error::Io)?);
2153 readers.push(ReftableReader::from_mapped(map)?);
2154 }
2155 Ok(CachedStackTables {
2156 table_names: table_names.to_owned(),
2157 readers,
2158 })
2159}
2160
2161fn build_cached_stack_tables_with_reload(
2164 reftable_dir: &Path,
2165 mut table_names: Vec<String>,
2166) -> Result<CachedStackTables> {
2167 let started = Instant::now();
2168 loop {
2169 match build_cached_stack_tables(reftable_dir, &table_names) {
2170 Ok(cached) => return Ok(cached),
2171 Err(e) if is_transient_stack_table_error(&e) => {
2172 if started.elapsed() >= STACK_TABLE_RELOAD_DEADLINE {
2173 return Err(e);
2174 }
2175 invalidate_stack_table_cache(reftable_dir);
2176 let fresh = read_tables_list_names(reftable_dir)?;
2177 let unchanged = fresh == table_names;
2178 table_names = fresh;
2179 if unchanged {
2180 return Err(e);
2181 }
2182 }
2183 Err(e) => return Err(e),
2184 }
2185 }
2186}
2187
2188fn load_cached_stack_tables(reftable_dir: &Path) -> Result<Arc<CachedStackTables>> {
2189 let key = tables_list_cache_key(reftable_dir)?;
2190 let mut guard = stack_table_cache()
2191 .lock()
2192 .map_err(|_| Error::Io(std::io::Error::other("reftable stack cache lock poisoned")))?;
2193 if let Some(hit) = guard.get(&key) {
2194 return Ok(Arc::clone(hit));
2195 }
2196
2197 let tables_list = reftable_dir.join("tables.list");
2198 let table_names: Vec<String> = match fs::read_to_string(&tables_list) {
2199 Ok(content) => content
2200 .lines()
2201 .filter(|l| !l.is_empty())
2202 .map(|l| l.to_owned())
2203 .collect(),
2204 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
2205 Err(e) => return Err(Error::Io(e)),
2206 };
2207 let cached = Arc::new(build_cached_stack_tables_with_reload(
2208 reftable_dir,
2209 table_names,
2210 )?);
2211 guard.insert(key, Arc::clone(&cached));
2212 Ok(cached)
2213}
2214
2215fn cached_stack_tables_for_names(
2221 reftable_dir: &Path,
2222 table_names: &[String],
2223) -> Result<Arc<CachedStackTables>> {
2224 let cached = load_cached_stack_tables(reftable_dir)?;
2225 if cached.table_names.as_slice() == table_names {
2226 return Ok(cached);
2227 }
2228 invalidate_stack_table_cache(reftable_dir);
2229 let cached = load_cached_stack_tables(reftable_dir)?;
2230 if cached.table_names.as_slice() == table_names {
2231 return Ok(cached);
2232 }
2233 let key = tables_list_cache_key(reftable_dir)?;
2234 let rebuilt = Arc::new(build_cached_stack_tables_with_reload(
2235 reftable_dir,
2236 table_names.to_vec(),
2237 )?);
2238 if let Ok(mut guard) = stack_table_cache().lock() {
2239 guard.insert(key, Arc::clone(&rebuilt));
2240 }
2241 Ok(rebuilt)
2242}
2243
2244fn cached_reader_for_name<'a>(
2245 cached: &'a CachedStackTables,
2246 table_name: &str,
2247) -> Result<&'a ReftableReader> {
2248 let idx = cached
2249 .table_names
2250 .iter()
2251 .position(|n| n == table_name)
2252 .ok_or_else(|| ReftableError::MissingTable {
2253 name: table_name.to_owned(),
2254 })?;
2255 Ok(&cached.readers[idx])
2256}
2257
2258pub struct ReftableStack {
2263 reftable_dir: PathBuf,
2265 table_names: Vec<String>,
2267 fixed_table_names: bool,
2269}
2270
2271fn parse_tables_list_content(content: &str) -> Vec<String> {
2272 content
2273 .lines()
2274 .filter(|l| !l.is_empty())
2275 .map(|l| l.to_owned())
2276 .collect()
2277}
2278
2279fn read_tables_list_names(reftable_dir: &Path) -> Result<Vec<String>> {
2280 let tables_list = reftable_dir.join("tables.list");
2281 match fs::read_to_string(&tables_list) {
2282 Ok(content) => Ok(parse_tables_list_content(&content)),
2283 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
2284 Err(e) => Err(Error::Io(e)),
2285 }
2286}
2287
2288pub(crate) struct TablesListLock {
2291 lock: Option<crate::lockfile::LockFile>,
2292}
2293
2294impl TablesListLock {
2295 fn new(lock: crate::lockfile::LockFile) -> Self {
2296 Self { lock: Some(lock) }
2297 }
2298
2299 fn path(&self) -> Result<&Path> {
2300 self.lock
2301 .as_ref()
2302 .map(|lock| lock.lock_path())
2303 .ok_or_else(|| ReftableError::LockReleased.into())
2304 }
2305}
2306
2307impl Drop for TablesListLock {
2308 fn drop(&mut self) {
2309 drop(self.lock.take());
2310 }
2311}
2312
2313impl ReftableStack {
2314 fn hash_size(&self) -> usize {
2317 match self.reftable_dir.parent() {
2318 Some(git_dir) => reftable_hash_size_for_git_dir(git_dir),
2319 None => 20,
2320 }
2321 }
2322
2323 pub fn open(git_dir: &Path) -> Result<Self> {
2328 let reftable_dir = git_dir.join("reftable");
2329 fs::create_dir_all(&reftable_dir).map_err(Error::Io)?;
2330 let tables_list = reftable_dir.join("tables.list");
2331 let table_names = match fs::read_to_string(&tables_list) {
2332 Ok(content) => parse_tables_list_content(&content),
2333 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
2334 Err(e) => return Err(Error::Io(e)),
2335 };
2336 Ok(Self {
2337 reftable_dir,
2338 table_names,
2339 fixed_table_names: false,
2340 })
2341 }
2342
2343 fn subset_stack(reftable_dir: PathBuf, table_names: Vec<String>) -> Self {
2344 Self {
2345 reftable_dir,
2346 table_names,
2347 fixed_table_names: true,
2348 }
2349 }
2350
2351 fn table_names_for_read(&self) -> Result<Vec<String>> {
2352 if self.fixed_table_names {
2353 Ok(self.table_names.clone())
2354 } else {
2355 read_tables_list_names(&self.reftable_dir)
2356 }
2357 }
2358
2359 fn first_ref_name_with_prefix(&self, prefix: &str) -> Result<Option<String>> {
2361 let names = self.table_names_for_read()?;
2362 let cached = cached_stack_tables_for_names(&self.reftable_dir, &names)?;
2363 let readers = self.stack_readers(&cached)?;
2364 let upper = prefix_upper_exclusive(prefix.trim_end_matches('/'));
2365 let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
2366 for reader in readers {
2367 reader.for_each_ref_in_range(prefix, upper.as_deref(), |rec| {
2368 if !rec.name.starts_with(prefix) || rec.name.len() <= prefix.len() {
2369 return ControlFlow::<()>::Continue(());
2370 }
2371 match &rec.value {
2372 RefValue::Deletion => {
2373 merged.remove(&rec.name);
2374 }
2375 _ => {
2376 merged.insert(rec.name.clone(), rec.clone());
2377 }
2378 }
2379 ControlFlow::<()>::Continue(())
2380 })?;
2381 }
2382 Ok(merged.keys().next().cloned())
2383 }
2384
2385 fn write_options(&self) -> WriteOptions {
2387 let git_dir = self
2388 .reftable_dir
2389 .parent()
2390 .map(|p| p.to_path_buf())
2391 .unwrap_or_else(|| self.reftable_dir.clone());
2392 read_write_options(&git_dir)
2393 }
2394
2395 fn stack_readers<'a>(&self, cached: &'a CachedStackTables) -> Result<Vec<&'a ReftableReader>> {
2396 cached
2397 .table_names
2398 .iter()
2399 .map(|name| cached_reader_for_name(cached, name))
2400 .collect()
2401 }
2402
2403 pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
2408 let names = self.table_names_for_read()?;
2409 let cached = cached_stack_tables_for_names(&self.reftable_dir, &names)?;
2410 let readers = self.stack_readers(&cached)?;
2411 let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
2412 for reader in readers {
2413 reader.for_each_ref(|rec| {
2414 match &rec.value {
2415 RefValue::Deletion => {
2416 merged.remove(&rec.name);
2417 }
2418 _ => {
2419 merged.insert(rec.name.clone(), rec.clone());
2420 }
2421 }
2422 ControlFlow::<()>::Continue(())
2423 })?;
2424 }
2425 Ok(merged.into_values().collect())
2426 }
2427
2428 pub(crate) fn for_each_merged_ref_under_prefix(
2433 &self,
2434 prefix: &str,
2435 mut f: impl FnMut(&RefRecord) -> ControlFlow<()>,
2436 ) -> Result<()> {
2437 let prefix = if prefix.is_empty() { "refs/" } else { prefix };
2438 let names = self.table_names_for_read()?;
2439 let cached = cached_stack_tables_for_names(&self.reftable_dir, &names)?;
2440 let readers = self.stack_readers(&cached)?;
2441 let lower = prefix;
2442 let upper = prefix_upper_exclusive(prefix);
2443
2444 let mut visit = |rec: &RefRecord| -> ControlFlow<()> {
2445 if matches!(rec.value, RefValue::Deletion) {
2446 return ControlFlow::Continue(());
2447 }
2448 if prefix.ends_with('/') && !rec.name.starts_with(prefix) {
2449 return ControlFlow::Continue(());
2450 }
2451 f(rec)
2452 };
2453
2454 if readers.len() == 1 {
2455 return readers[0].for_each_ref_in_range(lower, upper.as_deref(), |rec| visit(rec));
2456 }
2457
2458 let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
2459 for reader in readers {
2460 reader.for_each_ref_in_range(lower, upper.as_deref(), |rec| {
2461 if prefix.ends_with('/') && !rec.name.starts_with(prefix) {
2462 return ControlFlow::<()>::Continue(());
2463 }
2464 match &rec.value {
2465 RefValue::Deletion => {
2466 merged.remove(&rec.name);
2467 }
2468 _ => {
2469 merged.insert(rec.name.clone(), rec.clone());
2470 }
2471 }
2472 ControlFlow::<()>::Continue(())
2473 })?;
2474 }
2475 for rec in merged.values() {
2476 if visit(rec).is_break() {
2477 break;
2478 }
2479 }
2480 Ok(())
2481 }
2482
2483 pub fn list_refs_under_prefix(
2489 &self,
2490 git_dir: &Path,
2491 prefix: &str,
2492 ) -> Result<Vec<(String, ObjectId)>> {
2493 let mut out = Vec::new();
2494 self.for_each_merged_ref_under_prefix(prefix, |rec| {
2495 push_list_ref_entry(git_dir, rec, &mut out);
2496 ControlFlow::<()>::Continue(())
2497 })?;
2498 out.sort_by(|a, b| a.0.cmp(&b.0));
2499 Ok(out)
2500 }
2501
2502 fn table_payload_sizes(&self) -> Result<Vec<u64>> {
2503 let names = self.table_names_for_read()?;
2504 let cached = cached_stack_tables_for_names(&self.reftable_dir, &names)?;
2505 Ok(self
2506 .stack_readers(&cached)?
2507 .into_iter()
2508 .map(ReftableReader::payload_size)
2509 .collect())
2510 }
2511
2512 fn auto_compact_geometric(&mut self, opts: &WriteOptions) -> Result<()> {
2513 if !opts.autocompaction_enabled {
2514 return Ok(());
2515 }
2516 self.reload_table_names();
2517 if self.table_names.len() < 2 {
2518 return Ok(());
2519 }
2520 let sizes = self.table_payload_sizes()?;
2521 let (start, end) = suggest_compaction_segment(&sizes, opts.auto_compaction_factor);
2522 if end <= start + 1 {
2523 return Ok(());
2524 }
2525 self.compact_range(start, end - 1, opts)
2526 }
2527
2528 fn merge_refs_for_compaction(
2534 &self,
2535 table_names: &[String],
2536 preserve_deletion_tombstones: bool,
2537 ) -> Result<Vec<RefRecord>> {
2538 let cached = cached_stack_tables_for_names(&self.reftable_dir, table_names)?;
2539 let subset = Self::subset_stack(self.reftable_dir.clone(), table_names.to_vec());
2540 let readers = subset.stack_readers(&cached)?;
2541 let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
2542 for reader in readers {
2543 reader.for_each_ref(|rec| {
2544 match &rec.value {
2545 RefValue::Deletion => {
2546 if preserve_deletion_tombstones {
2547 merged.insert(rec.name.clone(), rec.clone());
2548 } else {
2549 merged.remove(&rec.name);
2550 }
2551 }
2552 _ => {
2553 merged.insert(rec.name.clone(), rec.clone());
2554 }
2555 }
2556 ControlFlow::<()>::Continue(())
2557 })?;
2558 }
2559 Ok(merged.into_values().collect())
2560 }
2561
2562 fn compact_range(&mut self, first: usize, last: usize, opts: &WriteOptions) -> Result<()> {
2563 let mut guard = self.acquire_tables_list_lock()?;
2564 self.reload_table_names();
2565 if first >= self.table_names.len() || last >= self.table_names.len() || first > last {
2566 return Ok(());
2567 }
2568 let compact_names: Vec<String> = self.table_names[first..=last].to_vec();
2569 let refs = self.merge_refs_for_compaction(&compact_names, first > 0)?;
2570 let prefix_stack = Self::subset_stack(self.reftable_dir.clone(), compact_names.clone());
2571 let logs = prefix_stack.read_all_logs()?;
2572
2573 let mut min_idx = u64::MAX;
2574 let mut max_idx = 0u64;
2575 for name in &compact_names {
2576 let path = self.reftable_dir.join(name);
2577 let data = fs::read(&path).map_err(Error::Io)?;
2578 let reader = ReftableReader::new(data)?;
2579 min_idx = min_idx.min(reader.min_update_index());
2580 max_idx = max_idx.max(reader.max_update_index());
2581 }
2582 if min_idx == u64::MAX {
2583 min_idx = 0;
2584 }
2585
2586 let wopts = WriteOptions {
2587 hash_size: self.hash_size(),
2588 ..opts.clone()
2589 };
2590 let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
2591 for rec in refs {
2592 writer.add_ref(rec)?;
2593 }
2594 for log in logs {
2595 writer.add_log(log)?;
2596 }
2597 let data = writer.finish()?;
2598 let filename = self.write_table_file(&data, min_idx, max_idx)?;
2599
2600 let mut new_names: Vec<String> = self.table_names[..first].to_vec();
2601 new_names.push(filename.clone());
2602 new_names.extend_from_slice(&self.table_names[last + 1..]);
2603 self.table_names = new_names;
2604 self.write_tables_list_locked(&mut guard)?;
2605 invalidate_stack_table_cache(&self.reftable_dir);
2606 for old in &compact_names {
2607 if old == &filename {
2608 continue;
2609 }
2610 let _ = fs::remove_file(self.reftable_dir.join(old));
2611 }
2612 Ok(())
2613 }
2614
2615 pub fn lookup_ref_oid(&self, name: &str) -> Result<Option<ObjectId>> {
2617 Ok(self.lookup_ref(name)?.and_then(|rec| match rec.value {
2618 RefValue::Val1(oid) => Some(oid),
2619 RefValue::Val2(oid, _) => Some(oid),
2620 RefValue::Deletion | RefValue::Symref(_) => None,
2621 }))
2622 }
2623
2624 pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
2626 let names = self.table_names_for_read()?;
2627 let cached = cached_stack_tables_for_names(&self.reftable_dir, &names)?;
2628 for reader in self.stack_readers(&cached)?.into_iter().rev() {
2629 if let Some(rec) = reader.lookup_ref(name)? {
2630 return match rec.value {
2631 RefValue::Deletion => Ok(None),
2632 _ => Ok(Some(rec)),
2633 };
2634 }
2635 }
2636 Ok(None)
2637 }
2638
2639 pub fn read_logs_for_ref(&self, refname: &str) -> Result<Vec<LogRecord>> {
2641 let started = Instant::now();
2642 loop {
2643 let table_names = self.table_names_for_read()?;
2644 match self.read_logs_for_ref_from_names(&table_names, refname) {
2645 Ok(logs) => return Ok(logs),
2646 Err(e) if is_transient_stack_table_error(&e) => {
2647 if started.elapsed() >= STACK_TABLE_RELOAD_DEADLINE {
2648 return Err(e);
2649 }
2650 invalidate_stack_table_cache(&self.reftable_dir);
2651 let fresh = self.table_names_for_read()?;
2652 if fresh == table_names {
2653 return Err(e);
2654 }
2655 }
2656 Err(e) => return Err(e),
2657 }
2658 }
2659 }
2660
2661 fn read_logs_for_ref_from_names(
2662 &self,
2663 table_names: &[String],
2664 refname: &str,
2665 ) -> Result<Vec<LogRecord>> {
2666 let Some(min_update_index) = self.min_log_update_index_for_live_ref(refname)? else {
2667 return Ok(Vec::new());
2668 };
2669 let mut logs = Vec::new();
2670 for table_name in table_names {
2671 let path = self.reftable_dir.join(table_name);
2672 let data = fs::read(&path).map_err(Error::Io)?;
2673 let reader = ReftableReader::new(data)?;
2674 for log in reader.read_logs()? {
2675 if log.refname == refname
2676 && log.update_index >= min_update_index
2677 && !log_record_is_deletion(&log)
2678 {
2679 logs.push(log);
2680 }
2681 }
2682 }
2683 logs.sort_by_key(|a| std::cmp::Reverse(a.update_index));
2684 Ok(logs)
2685 }
2686
2687 fn min_log_update_index_for_live_ref(&self, refname: &str) -> Result<Option<u64>> {
2692 let names = self.table_names_for_read()?;
2693 let cached = cached_stack_tables_for_names(&self.reftable_dir, &names)?;
2694 let readers = self.stack_readers(&cached)?;
2695 let mut events = Vec::new();
2696 for reader in &readers {
2697 reader.for_each_ref(|rec| {
2698 if rec.name == refname {
2699 events.push(rec.clone());
2700 }
2701 ControlFlow::<()>::Continue(())
2702 })?;
2703 }
2704 if events.is_empty() {
2705 return Ok(Some(0));
2706 }
2707 events.sort_by_key(|rec| rec.update_index);
2708 let mut min_log = 0u64;
2709 let mut live = false;
2710 for rec in events {
2711 match rec.value {
2712 RefValue::Deletion => {
2713 live = false;
2714 min_log = rec.update_index.saturating_add(1);
2715 }
2716 RefValue::Symref(_) | RefValue::Val1(_) | RefValue::Val2(..) => {
2717 live = true;
2718 }
2719 }
2720 }
2721 Ok(live.then_some(min_log))
2722 }
2723
2724 pub fn replace_logs_for_ref(
2726 &mut self,
2727 refname: &str,
2728 entries: &[crate::reflog::ReflogEntry],
2729 ) -> Result<()> {
2730 let entries = entries.to_vec();
2731 self.rewrite_logs_for_ref(refname, &mut |_| Ok(entries.clone()))
2732 }
2733
2734 pub fn rewrite_logs_for_ref(
2736 &mut self,
2737 refname: &str,
2738 transform: &mut dyn FnMut(
2739 Vec<crate::reflog::ReflogEntry>,
2740 ) -> Result<Vec<crate::reflog::ReflogEntry>>,
2741 ) -> Result<()> {
2742 let mut guard = self.acquire_tables_list_lock()?;
2743 self.reload_table_names();
2744 let current = self.read_reflog_entries_for_ref(refname)?;
2745 let entries = transform(current)?;
2746 self.replace_logs_for_ref_under_lock(&mut guard, refname, &entries)
2747 }
2748
2749 fn read_reflog_entries_for_ref(
2750 &self,
2751 refname: &str,
2752 ) -> Result<Vec<crate::reflog::ReflogEntry>> {
2753 let mut entries = Vec::new();
2754 for log in self.read_logs_for_ref(refname)? {
2755 let tz_sign = if log.tz_offset >= 0 { '+' } else { '-' };
2756 let tz_abs = log.tz_offset.unsigned_abs();
2757 let tz_hours = tz_abs / 100;
2758 let tz_mins = tz_abs % 100;
2759 let identity = format!(
2760 "{} <{}> {} {}{:02}{:02}",
2761 log.name, log.email, log.time_seconds, tz_sign, tz_hours, tz_mins
2762 );
2763 let message = log
2764 .message
2765 .strip_suffix('\n')
2766 .map(ToOwned::to_owned)
2767 .unwrap_or_else(|| log.message.clone());
2768 entries.push(crate::reflog::ReflogEntry {
2769 old_oid: log.old_id,
2770 new_oid: log.new_id,
2771 identity,
2772 message,
2773 });
2774 }
2775 entries.reverse();
2776 Ok(entries)
2777 }
2778
2779 fn replace_logs_for_ref_under_lock(
2780 &mut self,
2781 guard: &mut TablesListLock,
2782 refname: &str,
2783 entries: &[crate::reflog::ReflogEntry],
2784 ) -> Result<()> {
2785 let refs = self.read_refs()?;
2786 let mut logs: Vec<LogRecord> = self
2787 .read_all_logs()?
2788 .into_iter()
2789 .filter(|log| log.refname != refname)
2790 .collect();
2791 let mut next_update_index = self.max_update_index_unlocked()? + 1;
2792 let hash_size = self.hash_size();
2793 for entry in entries {
2794 let (name, email, time_secs, tz) = parse_identity_string(&entry.identity);
2795 logs.push(LogRecord {
2796 refname: refname.to_owned(),
2797 update_index: next_update_index,
2798 old_id: widen_oid_to(entry.old_oid, hash_size),
2799 new_id: widen_oid_to(entry.new_oid, hash_size),
2800 name,
2801 email,
2802 time_seconds: time_secs,
2803 tz_offset: tz,
2804 message: entry.message.clone(),
2805 });
2806 next_update_index += 1;
2807 }
2808
2809 let mut min_idx = u64::MAX;
2810 let mut max_idx = 0u64;
2811 for name in &self.table_names {
2812 let path = self.reftable_dir.join(name);
2813 let data = fs::read(&path).map_err(Error::Io)?;
2814 let reader = ReftableReader::new(data)?;
2815 min_idx = min_idx.min(reader.min_update_index());
2816 max_idx = max_idx.max(reader.max_update_index());
2817 }
2818 if min_idx == u64::MAX {
2819 min_idx = 0;
2820 }
2821 max_idx = max_idx.max(next_update_index.saturating_sub(1));
2822
2823 let wopts = WriteOptions {
2824 hash_size: self.hash_size(),
2825 ..Default::default()
2826 };
2827 let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
2828 for rec in refs {
2829 writer.add_ref(rec)?;
2830 }
2831 for log in logs {
2832 writer.add_log(log)?;
2833 }
2834 let data = writer.finish()?;
2835 let old_names = self.table_names.clone();
2836 let name = self.write_table_file(&data, min_idx, max_idx)?;
2837 self.table_names = vec![name];
2838 self.write_tables_list_locked(guard)?;
2839 for old in &old_names {
2840 let _ = fs::remove_file(self.reftable_dir.join(old));
2841 }
2842 Ok(())
2843 }
2844
2845 pub fn read_all_logs(&self) -> Result<Vec<LogRecord>> {
2847 let started = Instant::now();
2848 loop {
2849 let table_names = self.table_names_for_read()?;
2850 match self.read_all_logs_from_names(&table_names) {
2851 Ok(logs) => return Ok(logs),
2852 Err(e) if is_transient_stack_table_error(&e) => {
2853 if started.elapsed() >= STACK_TABLE_RELOAD_DEADLINE {
2854 return Err(e);
2855 }
2856 invalidate_stack_table_cache(&self.reftable_dir);
2857 let fresh = self.table_names_for_read()?;
2858 if fresh == table_names {
2859 return Err(e);
2860 }
2861 }
2862 Err(e) => return Err(e),
2863 }
2864 }
2865 }
2866
2867 fn read_all_logs_from_names(&self, table_names: &[String]) -> Result<Vec<LogRecord>> {
2868 let mut logs = Vec::new();
2869 for table_name in table_names {
2870 let path = self.reftable_dir.join(table_name);
2871 let data = fs::read(&path).map_err(Error::Io)?;
2872 let reader = ReftableReader::new(data)?;
2873 logs.extend(reader.read_logs()?);
2874 }
2875 logs.sort_by(|a, b| {
2876 a.refname
2877 .cmp(&b.refname)
2878 .then_with(|| b.update_index.cmp(&a.update_index))
2879 });
2880 Ok(logs)
2881 }
2882
2883 pub fn max_update_index(&self) -> Result<u64> {
2891 let names: Vec<String> = match fs::read_to_string(self.reftable_dir.join("tables.list")) {
2892 Ok(content) => content
2893 .lines()
2894 .filter(|line| !line.is_empty())
2895 .map(ToOwned::to_owned)
2896 .collect(),
2897 Err(_) => self.table_names.clone(),
2898 };
2899 let mut max_idx = 0u64;
2900 for name in &names {
2901 let path = self.reftable_dir.join(name);
2902 let data = match fs::read(&path) {
2903 Ok(data) => data,
2904 Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
2905 Err(err) => return Err(Error::Io(err)),
2906 };
2907 let reader = ReftableReader::new(data)?;
2908 max_idx = max_idx.max(reader.max_update_index());
2909 }
2910 Ok(max_idx)
2911 }
2912
2913 pub fn add_table(
2918 &mut self,
2919 data: &[u8],
2920 update_index: u64,
2921 opts: &WriteOptions,
2922 ) -> Result<String> {
2923 let reader = ReftableReader::new(data.to_vec())?;
2924 let table_has_deletion = reader
2925 .read_refs()?
2926 .iter()
2927 .any(|record| matches!(record.value, RefValue::Deletion));
2928 let min_idx = reader.min_update_index();
2929 let max_idx = reader.max_update_index();
2930 let _ = update_index;
2931 let filename = self.write_table_file(data, min_idx, max_idx)?;
2932
2933 {
2938 let mut guard = self.acquire_tables_list_lock()?;
2939 self.reload_table_names();
2940 self.table_names.push(filename.clone());
2941 self.write_tables_list_locked(&mut guard)?;
2942 }
2943
2944 if table_has_deletion && self.table_names.len() > 2 {
2948 self.compact_prefix_preserving_newest(opts)?;
2949 } else if opts.autocompaction_enabled {
2950 if self
2951 .table_names
2952 .iter()
2953 .any(|name| self.table_is_locked(name))
2954 {
2955 self.compact_unlocked_suffix()?;
2956 } else {
2957 self.auto_compact_geometric(opts)?;
2958 }
2959 }
2960
2961 Ok(filename)
2962 }
2963
2964 fn compact_prefix_preserving_newest(&mut self, opts: &WriteOptions) -> Result<()> {
2965 if !opts.autocompaction_enabled {
2966 return Ok(());
2967 }
2968 let mut guard = self.acquire_tables_list_lock()?;
2969 self.reload_table_names();
2970 if self.table_names.len() <= 2 {
2971 return Ok(());
2972 }
2973 let Some((newest, old_names)) = self.table_names.split_last() else {
2974 return Ok(());
2975 };
2976 let (newest, old_names) = (newest.clone(), old_names.to_vec());
2977 let prefix_stack = Self::subset_stack(self.reftable_dir.clone(), old_names.clone());
2978 let refs = prefix_stack.read_refs()?;
2979 let logs = prefix_stack.read_all_logs()?;
2980
2981 let mut min_idx = u64::MAX;
2982 let mut max_idx = 0u64;
2983 for name in &old_names {
2984 let path = self.reftable_dir.join(name);
2985 let data = fs::read(&path).map_err(Error::Io)?;
2986 let reader = ReftableReader::new(data)?;
2987 min_idx = min_idx.min(reader.min_update_index());
2988 max_idx = max_idx.max(reader.max_update_index());
2989 }
2990 if min_idx == u64::MAX {
2991 min_idx = 0;
2992 }
2993
2994 let wopts = WriteOptions {
2995 hash_size: self.hash_size(),
2996 ..Default::default()
2997 };
2998 let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
2999 for rec in refs {
3000 writer.add_ref(rec)?;
3001 }
3002 for log in logs {
3003 writer.add_log(log)?;
3004 }
3005 let data = writer.finish()?;
3006 let filename = self.write_table_file(&data, min_idx, max_idx)?;
3007 let keep: Vec<String> = vec![filename.clone(), newest.clone()];
3008 self.table_names = keep;
3009 self.write_tables_list_locked(&mut guard)?;
3010 for old in &old_names {
3011 if old == &filename || old == &newest {
3012 continue;
3013 }
3014 let _ = fs::remove_file(self.reftable_dir.join(old));
3015 }
3016 Ok(())
3017 }
3018
3019 fn table_is_locked(&self, name: &str) -> bool {
3020 self.reftable_dir.join(format!("{name}.lock")).exists()
3021 }
3022
3023 fn compact_unlocked_suffix(&mut self) -> Result<()> {
3024 let mut guard = self.acquire_tables_list_lock()?;
3025 self.reload_table_names();
3026 let first_unlocked = self
3027 .table_names
3028 .iter()
3029 .position(|name| !self.table_is_locked(name))
3030 .unwrap_or(self.table_names.len());
3031 if self.table_names.len().saturating_sub(first_unlocked) <= 1 {
3032 return Ok(());
3033 }
3034
3035 let locked_prefix: Vec<String> = self.table_names[..first_unlocked].to_vec();
3036 let old_suffix: Vec<String> = self.table_names[first_unlocked..].to_vec();
3037 let refs = self.merge_refs_for_compaction(&old_suffix, first_unlocked > 0)?;
3038 let suffix_stack = Self::subset_stack(self.reftable_dir.clone(), old_suffix.clone());
3039 let logs = suffix_stack.read_all_logs()?;
3040
3041 let mut min_idx = u64::MAX;
3042 let mut max_idx = 0u64;
3043 for name in &old_suffix {
3044 let path = self.reftable_dir.join(name);
3045 let data = fs::read(&path).map_err(Error::Io)?;
3046 let reader = ReftableReader::new(data)?;
3047 min_idx = min_idx.min(reader.min_update_index());
3048 max_idx = max_idx.max(reader.max_update_index());
3049 }
3050 if min_idx == u64::MAX {
3051 min_idx = 0;
3052 }
3053
3054 let wopts = WriteOptions {
3055 hash_size: self.hash_size(),
3056 ..Default::default()
3057 };
3058 let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
3059 for rec in refs {
3060 writer.add_ref(rec)?;
3061 }
3062 for log in logs {
3063 writer.add_log(log)?;
3064 }
3065 let data = writer.finish()?;
3066 let compacted = self.write_table_file(&data, min_idx, max_idx)?;
3067
3068 self.table_names = locked_prefix;
3069 self.table_names.push(compacted.clone());
3070 self.write_tables_list_locked(&mut guard)?;
3071 for old in &old_suffix {
3072 if old == &compacted {
3073 continue;
3074 }
3075 let _ = fs::remove_file(self.reftable_dir.join(old));
3076 }
3077 Ok(())
3078 }
3079
3080 pub fn write_ref(
3084 &mut self,
3085 refname: &str,
3086 value: RefValue,
3087 log: Option<LogRecord>,
3088 opts: &WriteOptions,
3089 ) -> Result<()> {
3090 {
3097 let mut guard = self.acquire_tables_list_lock()?;
3098 self.reload_table_names();
3099 let update_index = self.max_update_index_unlocked()? + 1;
3100 let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
3101 writer.add_ref(RefRecord {
3102 name: refname.to_owned(),
3103 update_index,
3104 value,
3105 })?;
3106 if let Some(log_rec) = log {
3107 let mut log_rec = log_rec;
3108 log_rec.update_index = update_index;
3109 writer.add_log(log_rec)?;
3110 }
3111 let data = writer.finish()?;
3112 let filename = self.write_table_file(&data, update_index, update_index)?;
3113 self.table_names.push(filename);
3114 self.write_tables_list_locked(&mut guard)?;
3115 }
3116
3117 self.maybe_auto_compact(opts)?;
3120 Ok(())
3121 }
3122
3123 pub fn write_transaction(
3129 &mut self,
3130 updates: Vec<ReftableTransactionUpdate>,
3131 opts: &WriteOptions,
3132 ) -> Result<()> {
3133 if updates.is_empty() {
3134 return Ok(());
3135 }
3136 let mut guard = self.acquire_tables_list_lock()?;
3137 self.write_transaction_under_lock(&mut guard, updates, opts)?;
3138 self.maybe_auto_compact(opts)?;
3139 Ok(())
3140 }
3141
3142 pub(crate) fn write_transaction_under_lock(
3144 &mut self,
3145 guard: &mut TablesListLock,
3146 mut updates: Vec<ReftableTransactionUpdate>,
3147 opts: &WriteOptions,
3148 ) -> Result<()> {
3149 if updates.is_empty() {
3150 return Ok(());
3151 }
3152 test_inject_reftable_transaction_fail()?;
3153
3154 self.reload_table_names();
3155 let cas_names: Vec<String> = updates
3156 .iter()
3157 .filter_map(|u| u.expected_old.map(|_| u.refname.clone()))
3158 .collect();
3159 if !cas_names.is_empty() {
3160 let current_by_name = self.lookup_ref_oids_batch(&cas_names)?;
3161 for update in &updates {
3162 if let Some(expected) = update.expected_old {
3163 let current = current_by_name.get(&update.refname).copied();
3164 if current != Some(expected) {
3165 return Err(ReftableError::ExpectedMismatch {
3166 refname: update.refname.clone(),
3167 expected,
3168 actual: current,
3169 }
3170 .into());
3171 }
3172 }
3173 }
3174 }
3175 let update_index = self.max_update_index_unlocked()? + 1;
3176 let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
3177
3178 updates.sort_by(|a, b| a.refname.cmp(&b.refname));
3179 for update in &updates {
3180 if let Some(value) = &update.value {
3181 writer.add_ref(RefRecord {
3182 name: update.refname.clone(),
3183 update_index,
3184 value: value.clone(),
3185 })?;
3186 }
3187 }
3188 for update in updates {
3189 if let Some(mut log) = update.log {
3190 log.update_index = update_index;
3191 writer.add_log(log)?;
3192 }
3193 }
3194
3195 let data = writer.finish()?;
3196 let filename = self.write_table_file(&data, update_index, update_index)?;
3197 self.table_names.push(filename);
3198 self.write_tables_list_locked(guard)?;
3199 Ok(())
3200 }
3201
3202 pub(crate) fn acquire_tables_list_lock_for_store(&self) -> Result<TablesListLock> {
3204 self.acquire_tables_list_lock()
3205 }
3206
3207 fn lookup_ref_oids_batch(&self, names: &[String]) -> Result<HashMap<String, ObjectId>> {
3208 let want: HashSet<String> = names.iter().cloned().collect();
3209 let mut found = HashMap::with_capacity(want.len());
3210 let mut resolved = HashSet::with_capacity(want.len());
3211 if want.is_empty() {
3212 return Ok(found);
3213 }
3214 let table_names = self.table_names_for_read()?;
3215 let cached = cached_stack_tables_for_names(&self.reftable_dir, &table_names)?;
3216 let readers = self.stack_readers(&cached)?;
3217 for reader in readers.iter().rev() {
3218 reader.for_each_ref(|rec| {
3219 if !want.contains(&rec.name) || resolved.contains(&rec.name) {
3220 return ControlFlow::<()>::Continue(());
3221 }
3222 match rec.value {
3223 RefValue::Val1(oid) | RefValue::Val2(oid, _) => {
3224 found.insert(rec.name.clone(), oid);
3225 resolved.insert(rec.name.clone());
3226 }
3227 RefValue::Deletion | RefValue::Symref(_) => {
3228 found.remove(&rec.name);
3229 resolved.insert(rec.name.clone());
3230 }
3231 }
3232 if resolved.len() == want.len() {
3233 return ControlFlow::Break(());
3234 }
3235 ControlFlow::<()>::Continue(())
3236 })?;
3237 if resolved.len() == want.len() {
3238 break;
3239 }
3240 }
3241 Ok(found)
3242 }
3243
3244 fn max_update_index_unlocked(&self) -> Result<u64> {
3248 let mut max_idx = 0u64;
3249 for name in &self.table_names {
3250 let path = self.reftable_dir.join(name);
3251 let data = match fs::read(&path) {
3252 Ok(data) => data,
3253 Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
3254 Err(err) => return Err(Error::Io(err)),
3255 };
3256 let reader = ReftableReader::new(data)?;
3257 max_idx = max_idx.max(reader.max_update_index());
3258 }
3259 Ok(max_idx)
3260 }
3261
3262 pub(crate) fn maybe_auto_compact(&mut self, opts: &WriteOptions) -> Result<()> {
3265 self.reload_table_names();
3266 if self
3267 .table_names
3268 .iter()
3269 .any(|name| self.table_is_locked(name))
3270 {
3271 self.compact_unlocked_suffix()?;
3272 return Ok(());
3273 }
3274 self.auto_compact_geometric(opts)?;
3275 Ok(())
3276 }
3277
3278 pub fn compact(&mut self) -> Result<()> {
3291 let mut guard = self.acquire_tables_list_lock()?;
3296 self.reload_table_names();
3297 if self.table_names.is_empty() {
3298 return Ok(());
3299 }
3300
3301 let refs = self.read_refs()?;
3303 let logs = self.read_all_logs()?;
3304
3305 let mut min_idx = u64::MAX;
3307 let mut max_idx = 0u64;
3308 for name in &self.table_names {
3309 let path = self.reftable_dir.join(name);
3310 let data = fs::read(&path).map_err(Error::Io)?;
3311 let reader = ReftableReader::new(data)?;
3312 min_idx = min_idx.min(reader.min_update_index());
3313 max_idx = max_idx.max(reader.max_update_index());
3314 }
3315 if min_idx == u64::MAX {
3316 min_idx = 0;
3317 }
3318
3319 let opts = self.write_options();
3322
3323 let mut writer = ReftableWriter::new(opts.clone(), min_idx, max_idx);
3324 for rec in refs {
3325 writer.add_ref(rec)?;
3326 }
3327 if opts.write_log {
3328 for log in logs {
3329 writer.add_log(log)?;
3330 }
3331 }
3332
3333 let data = writer.finish()?;
3334
3335 let old_names = self.table_names.clone();
3337 self.table_names.clear();
3338 let name = self.write_table_file(&data, min_idx, max_idx)?;
3339 self.table_names.push(name.clone());
3340 self.write_tables_list_locked(&mut guard)?;
3341
3342 for old in &old_names {
3344 if old == &name {
3345 continue;
3346 }
3347 let path = self.reftable_dir.join(old);
3348 let _ = fs::remove_file(&path);
3349 }
3350
3351 Ok(())
3352 }
3353
3354 fn write_table_file(
3355 &self,
3356 data: &[u8],
3357 min_update_index: u64,
3358 max_update_index: u64,
3359 ) -> Result<String> {
3360 const MAX_ATTEMPTS: u32 = 32;
3361 for _ in 0..MAX_ATTEMPTS {
3362 let suffix = random_table_suffix()?;
3363 match publish_new_reftable_table_file(
3364 &self.reftable_dir,
3365 data,
3366 min_update_index,
3367 max_update_index,
3368 suffix,
3369 ) {
3370 Ok(filename) => return Ok(filename),
3371 Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
3372 Err(err) => return Err(err),
3373 }
3374 }
3375 Err(Error::Io(std::io::Error::new(
3376 std::io::ErrorKind::AlreadyExists,
3377 "could not allocate a unique reftable table file name",
3378 )))
3379 }
3380
3381 #[expect(dead_code)]
3389 fn write_tables_list(&self) -> Result<()> {
3390 let mut guard = self.acquire_tables_list_lock()?;
3391 self.write_tables_list_locked(&mut guard)
3392 }
3393
3394 fn write_tables_list_locked(&self, guard: &mut TablesListLock) -> Result<()> {
3396 let tables_list = self.reftable_dir.join("tables.list");
3397 let content = self.table_names.join("\n")
3398 + if self.table_names.is_empty() {
3399 ""
3400 } else {
3401 "\n"
3402 };
3403 fs::write(guard.path()?, &content).map_err(Error::Io)?;
3404 let lock = guard.lock.take().ok_or(ReftableError::LockReleased)?;
3405 lock.commit_to(&tables_list).map_err(|err| match err {
3406 crate::lockfile::LockError::Io { source, .. } => Error::Io(source),
3407 crate::lockfile::LockError::Held { path } => {
3408 ReftableError::Locked { lock_path: path }.into()
3409 }
3410 })?;
3411 invalidate_stack_table_cache(&self.reftable_dir);
3412 Ok(())
3413 }
3414
3415 fn acquire_tables_list_lock(&self) -> Result<TablesListLock> {
3420 use crate::lockfile::{LockError, LockFile, LockWait};
3421 let lock = self.reftable_dir.join("tables.list.lock");
3422 let git_dir = self
3423 .reftable_dir
3424 .parent()
3425 .unwrap_or(self.reftable_dir.as_path());
3426 let config = ConfigSet::load(
3427 &crate::environment::Environment::empty(),
3428 Some(git_dir),
3429 true,
3430 )
3431 .unwrap_or_else(|_| ConfigSet::new());
3432 let wait = LockWait::reftable_tables_list(Some(&config));
3433 let file_lock = LockFile::acquire(&lock, wait).map_err(|err| match err {
3434 LockError::Held { path } => ReftableError::Locked { lock_path: path }.into(),
3435 LockError::Io { source, .. } => Error::Io(source),
3436 })?;
3437 Ok(TablesListLock::new(file_lock))
3438 }
3439
3440 pub(crate) fn reload_table_names(&mut self) {
3444 if let Ok(content) = fs::read_to_string(self.reftable_dir.join("tables.list")) {
3445 self.table_names = content
3446 .lines()
3447 .filter(|line| !line.is_empty())
3448 .map(ToOwned::to_owned)
3449 .collect();
3450 }
3451 }
3452
3453 pub fn table_names(&self) -> &[String] {
3455 &self.table_names
3456 }
3457}
3458
3459pub fn reftable_resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
3465 use crate::refs::store::RefStore;
3466 crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?.resolve(refname)
3467}
3468
3469pub(crate) fn reftable_storage_location(git_dir: &Path, refname: &str) -> (PathBuf, String) {
3470 if let Some(rest) = refname.strip_prefix("worktrees/") {
3471 if let Some((worktree_id, per_worktree_ref)) = rest.split_once('/') {
3472 if per_worktree_ref.starts_with("refs/") {
3473 let common =
3474 crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3475 return (
3476 common.join("worktrees").join(worktree_id),
3477 per_worktree_ref.to_owned(),
3478 );
3479 }
3480 }
3481 }
3482
3483 if refname == "HEAD"
3484 || refname.starts_with("refs/worktree/")
3485 || (git_dir.join("commondir").exists() && refname.starts_with("refs/bisect/"))
3486 {
3487 return (git_dir.to_path_buf(), refname.to_owned());
3488 }
3489
3490 (
3491 crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf()),
3492 refname.to_owned(),
3493 )
3494}
3495
3496pub fn reftable_write_ref(
3498 git_dir: &Path,
3499 refname: &str,
3500 oid: &ObjectId,
3501 log_identity: Option<&str>,
3502 log_message: Option<&str>,
3503) -> Result<()> {
3504 let (store_git_dir, _) = reftable_storage_location(git_dir, refname);
3505 let opts = read_write_options(&store_git_dir);
3506 reftable_write_ref_with_write_options(git_dir, refname, oid, log_identity, log_message, &opts)
3507}
3508
3509pub fn reftable_write_ref_with_write_options(
3511 git_dir: &Path,
3512 refname: &str,
3513 oid: &ObjectId,
3514 log_identity: Option<&str>,
3515 log_message: Option<&str>,
3516 opts: &WriteOptions,
3517) -> Result<()> {
3518 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
3519 let mut stack = ReftableStack::open(&store_git_dir)?;
3520 let old_oid = match stack
3521 .lookup_ref(&storage_refname)?
3522 .and_then(|r| match r.value {
3523 RefValue::Val1(oid) => Some(oid),
3524 RefValue::Val2(oid, _) => Some(oid),
3525 _ => None,
3526 }) {
3527 Some(oid) => oid,
3528 None => ObjectId::from_bytes(&vec![0u8; reftable_hash_size_for_git_dir(&store_git_dir)])?,
3529 };
3530
3531 let log = if let Some(identity) = log_identity {
3532 let (name, email, time_secs, tz) = parse_identity_string(identity);
3533 Some(LogRecord {
3534 refname: storage_refname.clone(),
3535 update_index: 0, old_id: old_oid,
3537 new_id: *oid,
3538 name,
3539 email,
3540 time_seconds: time_secs,
3541 tz_offset: tz,
3542 message: log_message.unwrap_or("").to_owned(),
3543 })
3544 } else {
3545 None
3546 };
3547
3548 stack.write_ref(&storage_refname, RefValue::Val1(*oid), log, opts)
3549}
3550
3551pub fn reftable_log_record_for_commit(
3557 git_dir: &Path,
3558 refname: &str,
3559 old_oid: &ObjectId,
3560 new_oid: &ObjectId,
3561 identity: &str,
3562 message: &str,
3563) -> Result<LogRecord> {
3564 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
3565 let opts = read_write_options(&store_git_dir);
3566 let (name, email, time_secs, tz) = parse_identity_string(identity);
3567 Ok(LogRecord {
3568 refname: storage_refname,
3569 update_index: 0,
3570 old_id: widen_oid_to(*old_oid, opts.hash_size),
3571 new_id: widen_oid_to(*new_oid, opts.hash_size),
3572 name,
3573 email,
3574 time_seconds: time_secs,
3575 tz_offset: tz,
3576 message: message.to_owned(),
3577 })
3578}
3579
3580pub fn reftable_write_symref(
3582 git_dir: &Path,
3583 refname: &str,
3584 target: &str,
3585 log_identity: Option<&str>,
3586 log_message: Option<&str>,
3587) -> Result<()> {
3588 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
3589 let mut stack = ReftableStack::open(&store_git_dir)?;
3590 let opts = read_write_options(&store_git_dir);
3591
3592 let log = if let Some(identity) = log_identity {
3593 let (name, email, time_secs, tz) = parse_identity_string(identity);
3594 let zero_oid =
3595 ObjectId::from_bytes(&vec![0u8; reftable_hash_size_for_git_dir(&store_git_dir)])?;
3596 Some(LogRecord {
3597 refname: storage_refname.clone(),
3598 update_index: 0,
3599 old_id: zero_oid,
3600 new_id: zero_oid,
3601 name,
3602 email,
3603 time_seconds: time_secs,
3604 tz_offset: tz,
3605 message: log_message.unwrap_or("").to_owned(),
3606 })
3607 } else {
3608 None
3609 };
3610
3611 stack.write_ref(
3612 &storage_refname,
3613 RefValue::Symref(target.to_owned()),
3614 log,
3615 &opts,
3616 )
3617}
3618
3619pub fn verify_ref_transaction_batch_reftable(
3623 git_dir: &Path,
3624 items: &[crate::refs::RefBatchItem],
3625) -> std::result::Result<(), crate::refs::RefnameUnavailable> {
3626 if items.iter().all(|i| i.new_oid.is_none()) {
3627 return Ok(());
3628 }
3629
3630 let (store_git_dir, _) = reftable_storage_location(git_dir, "HEAD");
3631 let Ok(stack) = ReftableStack::open(&store_git_dir) else {
3632 return Ok(());
3633 };
3634
3635 let extras: BTreeSet<String> = items.iter().map(|i| i.name.clone()).collect();
3636 let empty_skip: HashSet<String> = HashSet::new();
3637 let mut checked_ancestors: HashSet<String> = HashSet::new();
3638
3639 for item in items {
3640 let Some(_oid) = item.new_oid else {
3641 continue;
3642 };
3643
3644 let segments: Vec<&str> = item.name.split('/').filter(|s| !s.is_empty()).collect();
3645 if segments.len() > 1 {
3646 let mut dirname = String::new();
3647 for part in &segments[..segments.len() - 1] {
3648 if !dirname.is_empty() {
3649 dirname.push('/');
3650 }
3651 dirname.push_str(part);
3652 if !checked_ancestors.insert(dirname.clone()) {
3653 continue;
3654 }
3655 if empty_skip.contains(&dirname) {
3656 continue;
3657 }
3658 if extras.contains(&dirname) {
3659 return Err(crate::refs::RefnameUnavailable::SameBatch {
3660 refname: item.name.clone(),
3661 other: dirname.clone(),
3662 });
3663 }
3664 if stack.lookup_ref(&dirname).ok().flatten().is_some() {
3665 return Err(crate::refs::RefnameUnavailable::AncestorExists {
3666 blocking: dirname.clone(),
3667 new_ref: item.name.clone(),
3668 });
3669 }
3670 }
3671 }
3672
3673 if stack.lookup_ref_oid(&item.name).ok().flatten().is_some() {
3674 continue;
3675 }
3676
3677 if let Ok(Some(blocking)) = stack.first_ref_name_with_prefix(&format!("{}/", item.name)) {
3678 return Err(crate::refs::RefnameUnavailable::DescendantExists {
3679 blocking,
3680 new_ref: item.name.clone(),
3681 });
3682 }
3683 }
3684 Ok(())
3685}
3686
3687pub fn reftable_write_transaction(
3693 git_dir: &Path,
3694 updates: Vec<ReftableTransactionUpdate>,
3695) -> Result<()> {
3696 crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?
3697 .write_reftable_updates(updates)
3698 .map_err(Into::into)
3699}
3700
3701#[cfg(test)]
3702mod reftable_tx_fail_inject {
3703 use std::cell::RefCell;
3704
3705 thread_local! {
3707 pub static INJECT: RefCell<bool> = const { RefCell::new(false) };
3708 }
3709}
3710
3711#[cfg(test)]
3712mod reftable_write_test_hooks {
3713 use std::cell::RefCell;
3714 use std::collections::VecDeque;
3715 use std::path::Path;
3716
3717 thread_local! {
3718 static SUFFIXES: RefCell<VecDeque<u32>> = const { RefCell::new(VecDeque::new()) };
3719 static BEFORE_PERSIST: RefCell<Option<Box<dyn Fn(&Path)>>> = const { RefCell::new(None) };
3720 }
3721
3722 pub(crate) fn push_table_suffixes(values: impl IntoIterator<Item = u32>) {
3723 SUFFIXES.with(|s| s.borrow_mut().extend(values));
3724 }
3725
3726 pub(crate) fn take_table_suffix() -> Option<u32> {
3727 SUFFIXES.with(|s| s.borrow_mut().pop_front())
3728 }
3729
3730 pub(crate) fn set_before_persist_hook(hook: Option<Box<dyn Fn(&Path)>>) {
3731 BEFORE_PERSIST.with(|h| *h.borrow_mut() = hook);
3732 }
3733
3734 pub(crate) fn run_before_persist_hook(path: &Path) {
3735 BEFORE_PERSIST.with(|h| {
3736 if let Some(hook) = h.borrow().as_ref() {
3737 hook(path);
3738 }
3739 });
3740 }
3741}
3742
3743#[cfg(test)]
3745pub fn set_test_inject_reftable_transaction_fail(enabled: bool) {
3746 reftable_tx_fail_inject::INJECT.with(|c| *c.borrow_mut() = enabled);
3747}
3748
3749#[cfg(test)]
3750fn test_inject_reftable_transaction_fail() -> Result<()> {
3751 reftable_tx_fail_inject::INJECT.with(|c| {
3752 if *c.borrow() {
3753 return Err(Error::Io(std::io::Error::other(
3754 "injected reftable transaction failure",
3755 )));
3756 }
3757 Ok(())
3758 })
3759}
3760
3761#[cfg(not(test))]
3762fn test_inject_reftable_transaction_fail() -> Result<()> {
3763 Ok(())
3764}
3765
3766pub fn reftable_delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
3768 let (store_git_dir, _) = reftable_storage_location(git_dir, refname);
3769 let opts = read_write_options(&store_git_dir);
3770 reftable_delete_ref_with_write_options(git_dir, refname, &opts)
3771}
3772
3773pub fn reftable_delete_ref_with_write_options(
3775 git_dir: &Path,
3776 refname: &str,
3777 opts: &WriteOptions,
3778) -> Result<()> {
3779 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
3780 let mut stack = ReftableStack::open(&store_git_dir)?;
3781 if stack.lookup_ref(&storage_refname)?.is_none() {
3782 return Err(RefStoreError::not_found(refname).into());
3783 }
3784 stack.write_ref(&storage_refname, RefValue::Deletion, None, opts)
3785}
3786
3787pub fn reftable_read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
3789 crate::refs::read_symbolic_ref(git_dir, refname)
3790}
3791
3792fn push_list_ref_entry(git_dir: &Path, rec: &RefRecord, out: &mut Vec<(String, ObjectId)>) {
3793 match &rec.value {
3794 RefValue::Val1(oid) => out.push((rec.name.clone(), *oid)),
3795 RefValue::Val2(oid, _) => out.push((rec.name.clone(), *oid)),
3796 RefValue::Symref(target) => {
3797 if let Ok(oid) = reftable_resolve_ref(git_dir, target) {
3798 out.push((rec.name.clone(), oid));
3799 }
3800 }
3801 RefValue::Deletion => {}
3802 }
3803}
3804
3805pub fn reftable_list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
3807 let stack = ReftableStack::open(git_dir)?;
3808 stack.list_refs_under_prefix(git_dir, prefix)
3809}
3810
3811pub fn reftable_read_reflog(
3813 git_dir: &Path,
3814 refname: &str,
3815) -> Result<Vec<crate::reflog::ReflogEntry>> {
3816 use crate::refs::store::RefStore as _;
3817 let store = crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?;
3818 let mut entries = Vec::new();
3819 store.for_each_reflog_entry(refname, false, &mut |entry| {
3820 entries.push(entry.clone());
3821 std::ops::ControlFlow::Continue(())
3822 })?;
3823 Ok(entries)
3824}
3825
3826pub fn reftable_replace_reflog(
3828 git_dir: &Path,
3829 refname: &str,
3830 entries: &[crate::reflog::ReflogEntry],
3831) -> Result<()> {
3832 use crate::refs::store::RefStore as _;
3833 crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?
3834 .replace_reflog(refname, entries.to_vec())
3835}
3836
3837fn reftable_should_write_log(git_dir: &Path, storage_refname: &str) -> bool {
3843 let mode = crate::refs::effective_log_refs_config(git_dir);
3844 crate::refs::should_autocreate_reflog_for_mode(storage_refname, mode)
3845 || reftable_reflog_exists(git_dir, storage_refname)
3846}
3847
3848pub fn reftable_append_reflog(
3850 git_dir: &Path,
3851 refname: &str,
3852 old_oid: &ObjectId,
3853 new_oid: &ObjectId,
3854 identity: &str,
3855 message: &str,
3856 force_create: bool,
3857) -> Result<()> {
3858 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
3859 if !force_create && !reftable_should_write_log(&store_git_dir, &storage_refname) {
3868 return Ok(());
3869 }
3870 let (name, email, time_secs, tz) = parse_identity_string(identity);
3871 let mut stack = ReftableStack::open(&store_git_dir)?;
3872 let update_index = stack.max_update_index()? + 1;
3873 let opts = read_write_options(&store_git_dir);
3874
3875 let hash_size = opts.hash_size;
3879 let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
3880 writer.add_log(LogRecord {
3881 refname: storage_refname.clone(),
3882 update_index,
3883 old_id: widen_oid_to(*old_oid, hash_size),
3884 new_id: widen_oid_to(*new_oid, hash_size),
3885 name,
3886 email,
3887 time_seconds: time_secs,
3888 tz_offset: tz,
3889 message: message.to_owned(),
3890 })?;
3891
3892 let data = writer.finish()?;
3893 stack.add_table(&data, update_index, &opts)?;
3894 if storage_refname.starts_with("refs/heads/branch-") {
3895 stack.reload_table_names();
3896 let has_locked = stack
3897 .table_names
3898 .iter()
3899 .any(|name| stack.table_is_locked(name));
3900 if !has_locked && stack.table_names.len() <= 2 {
3901 stack.compact()?;
3902 }
3903 }
3904 Ok(())
3905}
3906
3907pub fn reftable_reflog_exists(git_dir: &Path, refname: &str) -> bool {
3909 use crate::refs::store::RefStore as _;
3910 crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())
3911 .ok()
3912 .and_then(|store| store.reflog_exists(refname).ok())
3913 .unwrap_or(false)
3914}
3915
3916pub fn reftable_list_reflog_refs(git_dir: &Path) -> Result<Vec<String>> {
3918 use crate::refs::store::RefStore as _;
3919 let store = crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?;
3920 let mut refs = Vec::new();
3921 store.for_each_reflog_ref(&mut |name| {
3922 refs.push(name.to_owned());
3923 std::ops::ControlFlow::Continue(())
3924 })?;
3925 refs.sort();
3926 Ok(refs)
3927}
3928
3929pub fn reftable_create_reflog(git_dir: &Path, refname: &str) -> Result<()> {
3931 use crate::refs::store::RefStore as _;
3932 crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?.create_reflog(refname)
3933}
3934
3935pub fn reftable_delete_reflog(git_dir: &Path, refname: &str) -> Result<()> {
3937 use crate::refs::store::RefStore as _;
3938 crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?.delete_reflog(refname)
3939}
3940
3941fn apply_geometric_factor_from_config(opts: &mut WriteOptions, raw: &str) {
3947 if let Ok(v) = raw.trim().parse::<u64>() {
3948 if (2..=255).contains(&v) {
3949 opts.auto_compaction_factor = v as u8;
3950 }
3951 }
3952}
3953
3954pub fn read_write_options(git_dir: &Path) -> WriteOptions {
3956 read_write_options_with_environment(git_dir, &crate::environment::Environment::empty())
3957}
3958
3959pub fn read_write_options_with_environment(
3961 git_dir: &Path,
3962 env: &crate::environment::Environment,
3963) -> WriteOptions {
3964 let mut opts = WriteOptions {
3965 hash_size: reftable_hash_size_for_git_dir(git_dir),
3966 autocompaction_enabled: env.reftable_autocompaction_enabled(),
3967 ..Default::default()
3968 };
3969
3970 let config = ConfigSet::load(env, Some(git_dir), true)
3971 .unwrap_or_else(|_| local_config_best_effort(git_dir));
3972 if let Some(Ok(v)) = config.get("reftable.blockSize").map(|v| v.parse::<u32>()) {
3973 opts.block_size = v;
3974 }
3975 if let Some(Ok(v)) = config
3976 .get("reftable.restartInterval")
3977 .map(|v| v.parse::<usize>())
3978 {
3979 opts.restart_interval = v;
3980 }
3981 if let Some(Ok(false)) = config.get_bool("reftable.indexObjects") {
3982 opts.skip_index_objects = true;
3983 }
3984 if let Some(value) = config.get("reftable.geometricFactor") {
3985 apply_geometric_factor_from_config(&mut opts, &value);
3986 }
3987 if config.log_refs_config() == crate::refs::LogRefsConfig::None {
3988 opts.write_log = false;
3989 }
3990 opts
3991}
3992
3993fn local_config_best_effort(git_dir: &Path) -> ConfigSet {
3996 let path = git_dir.join("config");
3997 let Ok(content) = fs::read_to_string(&path) else {
3998 return ConfigSet::new();
3999 };
4000 let (entries, _) = crate::config::ConfigFile::parse_gitmodules_best_effort(
4001 &path,
4002 &content,
4003 crate::config::ConfigScope::Local,
4004 );
4005 ConfigSet::from_entries(entries)
4006}
4007
4008pub fn dump_reftable_blocks(path: &Path) -> Result<String> {
4018 let data = fs::read(path).map_err(Error::Io)?;
4019 if data.len() < HEADER_SIZE {
4020 return Err(ReftableError::TooSmall { len: data.len() }.into());
4021 }
4022 if &data[0..4] != REFTABLE_MAGIC {
4023 return Err(ReftableError::BadMagic.into());
4024 }
4025 let version = data[4];
4026 let header_size = if version == 2 { 28 } else { 24 };
4027 let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
4028 let block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
4029
4030 let table_size = data.len().saturating_sub(footer_size);
4031
4032 let mut out = String::new();
4033 out.push_str("header:\n");
4034 out.push_str(&format!(" block_size: {block_size}\n"));
4035
4036 let mut section_type: u8 = 0;
4037 let mut block_off: u64 = 0;
4039 let mut first = true;
4040
4041 loop {
4042 if !first {
4043 }
4046 if block_off as usize >= table_size {
4047 break;
4048 }
4049 let header_off = if block_off == 0 { header_size } else { 0 };
4050 let pos = block_off as usize + header_off;
4051 if pos + 1 > data.len() {
4052 break;
4053 }
4054 let block_type = data[pos];
4055 let Some(section) = block_section_name(block_type) else {
4056 break;
4057 };
4058
4059 if pos + 4 > data.len() {
4061 break;
4062 }
4063 let blk_len =
4064 ((data[pos + 1] as u32) << 16) | ((data[pos + 2] as u32) << 8) | (data[pos + 3] as u32);
4065 let blk_len = blk_len as usize;
4066
4067 let (restart_off, restart_count, full_block_size) = if block_type == BLOCK_TYPE_LOG {
4069 let skip = 4 + header_off;
4072 let comp = &data[block_off as usize + skip..];
4073 let inflated_size = blk_len.saturating_sub(skip);
4074 let (inflated, consumed) = decompress_log_payload(comp, inflated_size)?;
4075 let mut full = vec![0u8; skip];
4077 full.extend_from_slice(&inflated);
4078 let rc = be16(&full, blk_len - 2) as usize;
4079 let roff = blk_len - 2 - 3 * rc;
4080 let fbs = skip + consumed;
4081 (roff, rc, fbs)
4082 } else {
4083 let abs = block_off as usize;
4084 if abs + blk_len < 2 {
4085 break;
4086 }
4087 let rc = be16(&data, abs + blk_len - 2) as usize;
4088 let roff = blk_len - 2 - 3 * rc;
4089 let mut fbs = block_size as usize;
4092 if fbs == 0 {
4093 fbs = blk_len;
4094 } else if blk_len < fbs
4095 && abs + blk_len < data.len()
4096 && data.get(abs + blk_len) == Some(&0u8)
4097 {
4098 } else if blk_len < fbs {
4100 fbs = blk_len;
4101 }
4102 (roff, rc, fbs)
4103 };
4104
4105 if block_type != section_type {
4106 section_type = block_type;
4107 out.push_str(&format!("{section}:\n"));
4108 }
4109
4110 out.push_str(&format!(" - length: {restart_off}\n"));
4111 out.push_str(&format!(" restarts: {restart_count}\n"));
4112
4113 block_off += full_block_size as u64;
4114 first = false;
4115 if full_block_size == 0 {
4116 break;
4117 }
4118 }
4119
4120 Ok(out)
4121}
4122
4123fn block_section_name(block_type: u8) -> Option<&'static str> {
4126 match block_type {
4127 BLOCK_TYPE_LOG => Some("log"),
4128 BLOCK_TYPE_REF => Some("ref"),
4129 BLOCK_TYPE_OBJ => Some("obj"),
4130 BLOCK_TYPE_INDEX => Some("idx"),
4131 _ => None,
4132 }
4133}
4134
4135fn be16(data: &[u8], off: usize) -> u16 {
4136 ((data[off] as u16) << 8) | (data[off + 1] as u16)
4137}
4138
4139fn decompress_log_payload(compressed: &[u8], inflated_size: usize) -> Result<(Vec<u8>, usize)> {
4141 use flate2::read::{DeflateDecoder, ZlibDecoder};
4142 use std::io::Read;
4143
4144 let mut inflated = vec![0u8; inflated_size];
4145 if compressed.len() >= 2 && compressed[0] == 0x78 {
4146 let mut decoder = ZlibDecoder::new(compressed);
4147 decoder
4148 .read_exact(&mut inflated)
4149 .map_err(|e| Error::Zlib(e.to_string()))?;
4150 Ok((inflated, decoder.total_in() as usize))
4151 } else {
4152 let mut decoder = DeflateDecoder::new(compressed);
4153 decoder
4154 .read_exact(&mut inflated)
4155 .map_err(|e| Error::Zlib(e.to_string()))?;
4156 Ok((inflated, decoder.total_in() as usize))
4157 }
4158}
4159
4160fn crc32(data: &[u8]) -> u32 {
4166 let mut crc: u32 = 0xffffffff;
4167 for &byte in data {
4168 crc ^= byte as u32;
4169 for _ in 0..8 {
4170 if crc & 1 != 0 {
4171 crc = (crc >> 1) ^ 0xedb88320;
4172 } else {
4173 crc >>= 1;
4174 }
4175 }
4176 }
4177 !crc
4178}
4179
4180fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
4182 a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
4183}
4184
4185fn read_u24(data: &[u8], pos: usize) -> usize {
4187 ((data[pos] as usize) << 16) | ((data[pos + 1] as usize) << 8) | (data[pos + 2] as usize)
4188}
4189
4190fn read_u16(data: &[u8], pos: usize) -> usize {
4192 ((data[pos] as usize) << 8) | (data[pos + 1] as usize)
4193}
4194
4195fn restart_table_start_for_block(
4200 data: &[u8],
4201 records_end: usize,
4202 records_data_start: usize,
4203) -> Result<usize> {
4204 if records_end < 2 || records_end > data.len() {
4205 return Err(corrupt(ReftableError::InvalidBlockLength));
4206 }
4207 let rc = read_u16(data, records_end - 2);
4208 if rc > MAX_RESTARTS {
4209 return Err(corrupt(ReftableError::InvalidRestartTable));
4210 }
4211 let restart_bytes = rc * 3;
4212 let Some(restart_table_start) = records_end
4213 .checked_sub(2)
4214 .and_then(|without_count| without_count.checked_sub(restart_bytes))
4215 else {
4216 return Err(corrupt(ReftableError::InvalidRestartTable));
4217 };
4218 if restart_table_start < records_data_start {
4219 return Err(corrupt(ReftableError::InvalidRestartTable));
4220 }
4221 Ok(restart_table_start)
4222}
4223
4224fn parse_footer(data: &[u8], version: u8) -> Result<Footer> {
4226 let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
4227 if data.len() < footer_size {
4228 return Err(ReftableError::TooSmall { len: data.len() }.into());
4229 }
4230
4231 if &data[0..4] != REFTABLE_MAGIC {
4233 return Err(ReftableError::BadMagic.into());
4234 }
4235 let fver = data[4];
4236 if fver != version {
4237 return Err(ReftableError::FooterVersionMismatch {
4238 header: version,
4239 footer: fver,
4240 }
4241 .into());
4242 }
4243
4244 let read_u64 = |slice: &[u8]| -> Result<u64> {
4247 let bytes: [u8; 8] = slice
4248 .try_into()
4249 .map_err(|_| truncated(ReftableField::Footer))?;
4250 Ok(u64::from_be_bytes(bytes))
4251 };
4252
4253 let block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
4254 let min_update_index = read_u64(&data[8..16])?;
4255 let max_update_index = read_u64(&data[16..24])?;
4256
4257 let off = if version == 2 { 28 } else { 24 };
4260 let ref_index_position = read_u64(&data[off..off + 8])?;
4261 let log_position = read_u64(&data[off + 24..off + 32])?;
4262
4263 let crc_bytes: [u8; 4] = data[footer_size - 4..footer_size]
4265 .try_into()
4266 .map_err(|_| truncated(ReftableField::Footer))?;
4267 let crc_stored = u32::from_be_bytes(crc_bytes);
4268 let crc_computed = crc32(&data[..footer_size - 4]);
4269 if crc_stored != crc_computed {
4270 return Err(ReftableError::FooterChecksumMismatch {
4271 stored: crc_stored,
4272 computed: crc_computed,
4273 }
4274 .into());
4275 }
4276
4277 Ok(Footer {
4278 block_size,
4279 min_update_index,
4280 max_update_index,
4281 ref_index_position,
4282 log_position,
4283 })
4284}
4285
4286pub(crate) fn parse_identity_string(identity: &str) -> (String, String, u64, i16) {
4288 let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
4290 if parts.len() < 3 {
4291 return (identity.to_owned(), String::new(), 0, 0);
4292 }
4293 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);
4298
4299 let tz_hhmm = if tz_str.len() >= 5 {
4302 let sign = if tz_str.starts_with('-') { -1i16 } else { 1 };
4303 let hours = tz_str[1..3].parse::<i16>().unwrap_or(0);
4304 let mins = tz_str[3..5].parse::<i16>().unwrap_or(0);
4305 sign * (hours * 100 + mins)
4306 } else {
4307 0
4308 };
4309
4310 let (name, email) = if let Some(lt_pos) = name_email.find('<') {
4312 let name = name_email[..lt_pos].trim().to_owned();
4313 let email = if let Some(gt_pos) = name_email.find('>') {
4314 name_email[lt_pos + 1..gt_pos].to_owned()
4315 } else {
4316 name_email[lt_pos + 1..].to_owned()
4317 };
4318 (name, email)
4319 } else {
4320 (name_email.to_owned(), String::new())
4321 };
4322
4323 (name, email, time_secs, tz_hhmm)
4324}
4325
4326#[cfg(test)]
4331mod tests {
4332 use super::*;
4333
4334 fn reftable_repo_with_config(extra: &str) -> (tempfile::TempDir, PathBuf) {
4335 let dir = tempfile::tempdir().expect("tempdir");
4336 crate::repo::init_repository(
4337 dir.path(),
4338 false,
4339 "main",
4340 None,
4341 crate::RefStorageFormat::Reftable,
4342 )
4343 .expect("init");
4344 let git_dir = dir.path().join(".git");
4345 let mut cfg = fs::read_to_string(git_dir.join("config")).expect("read config");
4346 cfg.push_str(extra);
4347 fs::write(git_dir.join("config"), cfg).expect("write config");
4348 (dir, git_dir)
4349 }
4350
4351 #[test]
4352 fn write_options_log_blocks_follow_log_all_ref_updates() {
4353 for (value, write_log) in [
4354 ("true", true),
4355 ("always", true),
4356 ("yes", true),
4357 ("on", true),
4358 ("1", true),
4359 ("false", false),
4360 ("no", false),
4361 ("0", false),
4362 ("never", false),
4363 ] {
4364 let (_dir, git_dir) =
4365 reftable_repo_with_config(&format!("[core]\n\tlogAllRefUpdates = {value}\n"));
4366 assert_eq!(
4367 read_write_options(&git_dir).write_log,
4368 write_log,
4369 "core.logAllRefUpdates = {value}"
4370 );
4371 }
4372 let (_dir, git_dir) = reftable_repo_with_config("");
4373 assert!(read_write_options(&git_dir).write_log);
4374 }
4375
4376 #[test]
4377 fn write_options_broken_config_keeps_entries_before_bad_line() {
4378 let (_dir, git_dir) = reftable_repo_with_config(
4379 "[reftable]\n\tgeometricFactor = 5\n\tindexObjects = off\n[core]\n\tlogAllRefUpdates = no\n[broken\nblockSize = 1\n",
4380 );
4381 let opts = read_write_options(&git_dir);
4382 assert_eq!(opts.auto_compaction_factor, 5);
4383 assert!(opts.skip_index_objects);
4384 assert!(!opts.write_log);
4385 assert_eq!(opts.block_size, WriteOptions::default().block_size);
4386 }
4387
4388 #[test]
4389 fn should_write_log_follows_mode_and_existing_reflog() {
4390 let (_dir, git_dir) = reftable_repo_with_config("[core]\n\tlogAllRefUpdates = true\n");
4391 assert!(reftable_should_write_log(&git_dir, "refs/heads/main"));
4392 assert!(!reftable_should_write_log(&git_dir, "refs/tags/v1"));
4393
4394 let (_dir, git_dir) = reftable_repo_with_config("[core]\n\tlogAllRefUpdates = always\n");
4395 assert!(reftable_should_write_log(&git_dir, "refs/tags/v1"));
4396
4397 let (_dir, git_dir) = reftable_repo_with_config("[core]\n\tlogAllRefUpdates = false\n");
4398 assert!(!reftable_should_write_log(&git_dir, "refs/heads/topic"));
4399 reftable_create_reflog(&git_dir, "refs/heads/topic").expect("create reflog");
4400 assert!(reftable_should_write_log(&git_dir, "refs/heads/topic"));
4401 }
4402
4403 #[test]
4404 fn stack_table_cache_readers_use_mapped_backing() {
4405 use crate::objects::ObjectId;
4406 use crate::repo::init_repository;
4407
4408 let dir = tempfile::tempdir().expect("tempdir");
4409 init_repository(
4410 dir.path(),
4411 false,
4412 "main",
4413 None,
4414 crate::RefStorageFormat::Reftable,
4415 )
4416 .expect("init");
4417 let git_dir = dir.path().join(".git");
4418 let oid = ObjectId::from_bytes(&[0x44; 20]).expect("oid");
4419 reftable_write_ref(&git_dir, "refs/heads/mapped-test", &oid, None, None)
4420 .expect("write ref");
4421 let reftable_dir = git_dir.join("reftable");
4422 let cached = super::load_cached_stack_tables(&reftable_dir).expect("cache");
4423 assert!(
4424 cached.readers.iter().all(|r| r.table_backing_is_mapped()),
4425 "stack cache must mmap tables instead of copying them into Arc<[u8]>"
4426 );
4427 let again = super::load_cached_stack_tables(&reftable_dir).expect("cache again");
4428 assert!(
4429 Arc::ptr_eq(&cached, &again),
4430 "second load must hit the process-wide stack cache"
4431 );
4432 }
4433
4434 #[test]
4435 fn read_refs_on_empty_reftable_without_tables_list() {
4436 let dir = tempfile::tempdir().expect("tempdir");
4437 let git_dir = dir.path().join(".git");
4438 fs::create_dir_all(git_dir.join("reftable")).expect("reftable dir");
4439 let stack = ReftableStack::open(&git_dir).expect("open empty stack");
4440 assert!(stack.read_refs().expect("read").is_empty());
4441 }
4442
4443 #[test]
4444 fn read_refs_tracks_external_compaction_on_public_stack() {
4445 use crate::objects::ObjectId;
4446 use crate::repo::init_repository;
4447
4448 let dir = tempfile::tempdir().expect("tempdir");
4449 init_repository(
4450 dir.path(),
4451 false,
4452 "main",
4453 None,
4454 crate::RefStorageFormat::Reftable,
4455 )
4456 .expect("init");
4457 let git_dir = dir.path().join(".git");
4458 let mut opts = read_write_options(&git_dir);
4459 opts.autocompaction_enabled = false;
4460 let oid_a = ObjectId::from_bytes(&[0x31; 20]).expect("oid");
4461 let oid_b = ObjectId::from_bytes(&[0x32; 20]).expect("oid");
4462 let identity = "Compact Ext <ext@example.com> 1700000000 +0000";
4463 reftable_write_ref_with_write_options(
4464 &git_dir,
4465 "refs/heads/keep-a",
4466 &oid_a,
4467 Some(identity),
4468 None,
4469 &opts,
4470 )
4471 .expect("write a");
4472 reftable_write_ref_with_write_options(
4473 &git_dir,
4474 "refs/heads/keep-b",
4475 &oid_b,
4476 Some(identity),
4477 None,
4478 &opts,
4479 )
4480 .expect("write b");
4481
4482 let reader = ReftableStack::open(&git_dir).expect("reader stack");
4483 let mut writer = ReftableStack::open(&git_dir).expect("writer stack");
4484 writer.compact().expect("compact via second handle");
4485 let refs = reader.read_refs().expect("read after external compact");
4486 let names: Vec<&str> = refs.iter().map(|r| r.name.as_str()).collect();
4487 assert!(names.iter().any(|n| *n == "refs/heads/keep-a"));
4488 assert!(names.iter().any(|n| *n == "refs/heads/keep-b"));
4489 }
4490
4491 #[test]
4492 fn subset_stack_read_refs_respects_table_names() {
4493 use crate::objects::ObjectId;
4494 use crate::repo::init_repository;
4495
4496 let dir = tempfile::tempdir().expect("tempdir");
4497 init_repository(
4498 dir.path(),
4499 false,
4500 "main",
4501 None,
4502 crate::RefStorageFormat::Reftable,
4503 )
4504 .expect("init");
4505 let git_dir = dir.path().join(".git");
4506 let mut opts = read_write_options(&git_dir);
4507 opts.autocompaction_enabled = false;
4508 let oid_a = ObjectId::from_bytes(&[0x11; 20]).expect("oid");
4509 let oid_b = ObjectId::from_bytes(&[0x22; 20]).expect("oid");
4510 let identity = "Subset Stack <subset@example.com> 1700000000 +0000";
4511 reftable_write_ref_with_write_options(
4512 &git_dir,
4513 "refs/heads/only-in-first-table",
4514 &oid_a,
4515 Some(identity),
4516 None,
4517 &opts,
4518 )
4519 .expect("first ref");
4520 let first_table = ReftableStack::open(&git_dir)
4521 .expect("stack after first ref")
4522 .table_names
4523 .last()
4524 .expect("first ref table")
4525 .clone();
4526 reftable_write_ref_with_write_options(
4527 &git_dir,
4528 "refs/heads/only-in-second-table",
4529 &oid_b,
4530 Some(identity),
4531 None,
4532 &opts,
4533 )
4534 .expect("second ref");
4535
4536 let stack = ReftableStack::open(&git_dir).expect("open");
4537 assert!(
4538 stack.table_names.len() >= 2,
4539 "expected multiple tables without autocompaction, got {}",
4540 stack.table_names.len()
4541 );
4542
4543 let subset = ReftableStack::subset_stack(stack.reftable_dir.clone(), vec![first_table]);
4544 let refs = subset.read_refs().expect("subset read");
4545 let names: Vec<&str> = refs.iter().map(|r| r.name.as_str()).collect();
4546 assert!(names.iter().any(|n| *n == "refs/heads/only-in-first-table"));
4547 assert!(!names
4548 .iter()
4549 .any(|n| *n == "refs/heads/only-in-second-table"));
4550
4551 let full = stack.read_refs().expect("full read");
4552 assert_eq!(full.len(), 3);
4553 assert!(full.iter().any(|record| record.name == "HEAD"
4554 && record.value == RefValue::Symref("refs/heads/main".into())));
4555 }
4556
4557 #[test]
4558 fn compact_range_non_bottom_preserves_deletion_tombstone() {
4559 use crate::objects::ObjectId;
4560 use crate::repo::init_repository;
4561
4562 let dir = tempfile::tempdir().expect("tempdir");
4563 init_repository(
4564 dir.path(),
4565 false,
4566 "main",
4567 None,
4568 crate::RefStorageFormat::Reftable,
4569 )
4570 .expect("init");
4571 let git_dir = dir.path().join(".git");
4572 let mut opts = read_write_options(&git_dir);
4573 opts.autocompaction_enabled = false;
4574 let oid_c = ObjectId::from_bytes(&[0xcc; 20]).expect("oid");
4575 let oid_other = ObjectId::from_bytes(&[0xdd; 20]).expect("oid");
4576 let identity = "Compact Tomb <tomb@example.com> 1700000000 +0000";
4577 reftable_write_ref_with_write_options(
4578 &git_dir,
4579 "refs/heads/c",
4580 &oid_c,
4581 Some(identity),
4582 None,
4583 &opts,
4584 )
4585 .expect("create c");
4586 reftable_write_ref_with_write_options(
4587 &git_dir,
4588 "refs/heads/unrelated",
4589 &oid_other,
4590 Some(identity),
4591 None,
4592 &opts,
4593 )
4594 .expect("update unrelated");
4595 reftable_delete_ref_with_write_options(&git_dir, "refs/heads/c", &opts).expect("delete c");
4596
4597 let mut stack = ReftableStack::open(&git_dir).expect("open");
4598 assert!(
4599 stack.table_names.len() >= 4,
4600 "need init plus three ref tables, got {}",
4601 stack.table_names.len()
4602 );
4603 let bottom = stack.table_names[0].clone();
4604 let table_with_c = stack.table_names[1].clone();
4605 let last = stack.table_names.len() - 1;
4606 stack
4607 .compact_range(last - 1, last, &opts)
4608 .expect("compact suffix with delete");
4609
4610 assert!(stack.lookup_ref("refs/heads/c").expect("lookup").is_none());
4611 let compacted_name = stack.table_names[last - 1].clone();
4612 assert_ne!(compacted_name, bottom);
4613 assert_ne!(compacted_name, table_with_c);
4614 let compacted_data = fs::read(stack.reftable_dir.join(&compacted_name)).expect("read");
4615 let compacted_reader = ReftableReader::new(compacted_data).expect("reader");
4616 let compacted_refs = compacted_reader.read_refs().expect("refs");
4617 assert!(
4618 compacted_refs
4619 .iter()
4620 .any(|rec| { rec.name == "refs/heads/c" && rec.value == RefValue::Deletion }),
4621 "compacted table must retain delete tombstone: {compacted_refs:?}"
4622 );
4623 let bottom_data = fs::read(stack.reftable_dir.join(&bottom)).expect("bottom");
4624 let bottom_reader = ReftableReader::new(bottom_data).expect("bottom reader");
4625 assert!(
4626 !bottom_reader
4627 .read_refs()
4628 .expect("bottom refs")
4629 .iter()
4630 .any(|rec| rec.name == "refs/heads/c"),
4631 "init table must remain untouched"
4632 );
4633 let c_table_data = fs::read(stack.reftable_dir.join(&table_with_c)).expect("c table");
4634 let c_table_reader = ReftableReader::new(c_table_data).expect("c table reader");
4635 assert!(
4636 c_table_reader
4637 .read_refs()
4638 .expect("c table refs")
4639 .iter()
4640 .any(|rec| rec.name == "refs/heads/c"),
4641 "older table must still hold the ref until shadowed"
4642 );
4643 }
4644
4645 #[test]
4646 fn compact_range_from_bottom_omits_deletion_tombstone() {
4647 use crate::objects::ObjectId;
4648 use crate::repo::init_repository;
4649
4650 let dir = tempfile::tempdir().expect("tempdir");
4651 init_repository(
4652 dir.path(),
4653 false,
4654 "main",
4655 None,
4656 crate::RefStorageFormat::Reftable,
4657 )
4658 .expect("init");
4659 let git_dir = dir.path().join(".git");
4660 let mut opts = read_write_options(&git_dir);
4661 opts.autocompaction_enabled = false;
4662 let oid_c = ObjectId::from_bytes(&[0xcc; 20]).expect("oid");
4663 let oid_other = ObjectId::from_bytes(&[0xdd; 20]).expect("oid");
4664 let identity = "Compact Tomb <tomb@example.com> 1700000000 +0000";
4665 reftable_write_ref_with_write_options(
4666 &git_dir,
4667 "refs/heads/c",
4668 &oid_c,
4669 Some(identity),
4670 None,
4671 &opts,
4672 )
4673 .expect("create c");
4674 reftable_write_ref_with_write_options(
4675 &git_dir,
4676 "refs/heads/unrelated",
4677 &oid_other,
4678 Some(identity),
4679 None,
4680 &opts,
4681 )
4682 .expect("update unrelated");
4683 reftable_delete_ref_with_write_options(&git_dir, "refs/heads/c", &opts).expect("delete c");
4684
4685 let mut stack = ReftableStack::open(&git_dir).expect("open");
4686 let table_count = stack.table_names.len();
4687 stack
4688 .compact_range(0, table_count - 1, &opts)
4689 .expect("compact all");
4690 assert!(stack.lookup_ref("refs/heads/c").expect("lookup").is_none());
4691 assert_eq!(stack.table_names.len(), 1);
4692 let only = ReftableReader::new(
4693 fs::read(stack.reftable_dir.join(&stack.table_names[0])).expect("read"),
4694 )
4695 .expect("reader");
4696 assert!(
4697 !only
4698 .read_refs()
4699 .expect("refs")
4700 .iter()
4701 .any(|rec| rec.name == "refs/heads/c"),
4702 "full-stack compaction must not store delete tombstones"
4703 );
4704 }
4705
4706 #[test]
4707 fn suggest_compaction_segment_matches_geometric_break() {
4708 assert_eq!(suggest_compaction_segment(&[10], 2), (0, 0));
4709 assert_eq!(suggest_compaction_segment(&[64, 32, 16], 2), (0, 0));
4710 let (start, end) = suggest_compaction_segment(&[64, 32, 16, 8, 4, 3, 1], 2);
4711 assert!(
4712 end > start + 1,
4713 "expected compaction segment, got {start}..{end}"
4714 );
4715 }
4716
4717 #[test]
4718 fn prefix_upper_exclusive_for_seek_end() {
4719 assert_eq!(
4720 prefix_upper_exclusive("refs/heads/b05").as_deref(),
4721 Some("refs/heads/b06")
4722 );
4723 assert!(prefix_upper_exclusive("refs/heads/").is_none());
4724 }
4725
4726 #[test]
4727 fn test_varint_roundtrip() {
4728 for val in [0u64, 1, 127, 128, 255, 256, 16383, 16384, u64::MAX] {
4729 let mut buf = Vec::new();
4730 put_varint(val, &mut buf);
4731 let (decoded, end) = get_varint(&buf, 0).unwrap();
4732 assert_eq!(decoded, val, "varint roundtrip failed for {val}");
4733 assert_eq!(end, buf.len());
4734 }
4735 }
4736
4737 #[test]
4738 fn test_crc32() {
4739 assert_eq!(crc32(b"123456789"), 0xCBF43926);
4741 }
4742
4743 #[test]
4744 fn test_empty_table() {
4745 let writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
4746 let data = writer.finish().unwrap();
4747 let reader = ReftableReader::new(data).unwrap();
4748 let refs = reader.read_refs().unwrap();
4749 assert!(refs.is_empty());
4750 }
4751
4752 #[test]
4753 fn test_write_read_single_ref() {
4754 let oid = ObjectId::from_bytes(&[0xab; 20]).unwrap();
4755 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
4756 writer
4757 .add_ref(RefRecord {
4758 name: "refs/heads/main".to_owned(),
4759 update_index: 1,
4760 value: RefValue::Val1(oid),
4761 })
4762 .unwrap();
4763 let data = writer.finish().unwrap();
4764
4765 let reader = ReftableReader::new(data).unwrap();
4766 let refs = reader.read_refs().unwrap();
4767 assert_eq!(refs.len(), 1);
4768 assert_eq!(refs[0].name, "refs/heads/main");
4769 assert_eq!(refs[0].value, RefValue::Val1(oid));
4770 assert_eq!(refs[0].update_index, 1);
4771 }
4772
4773 #[test]
4774 fn test_write_read_multiple_refs() {
4775 let oid1 = ObjectId::from_bytes(&[0x11; 20]).unwrap();
4776 let oid2 = ObjectId::from_bytes(&[0x22; 20]).unwrap();
4777 let oid3 = ObjectId::from_bytes(&[0x33; 20]).unwrap();
4778
4779 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
4780 writer
4781 .add_ref(RefRecord {
4782 name: "refs/heads/a".to_owned(),
4783 update_index: 1,
4784 value: RefValue::Val1(oid1),
4785 })
4786 .unwrap();
4787 writer
4788 .add_ref(RefRecord {
4789 name: "refs/heads/b".to_owned(),
4790 update_index: 1,
4791 value: RefValue::Val1(oid2),
4792 })
4793 .unwrap();
4794 writer
4795 .add_ref(RefRecord {
4796 name: "refs/tags/v1.0".to_owned(),
4797 update_index: 1,
4798 value: RefValue::Val2(oid3, oid1),
4799 })
4800 .unwrap();
4801 let data = writer.finish().unwrap();
4802
4803 let reader = ReftableReader::new(data).unwrap();
4804 let refs = reader.read_refs().unwrap();
4805 assert_eq!(refs.len(), 3);
4806 assert_eq!(refs[0].name, "refs/heads/a");
4807 assert_eq!(refs[1].name, "refs/heads/b");
4808 assert_eq!(refs[2].name, "refs/tags/v1.0");
4809 assert_eq!(refs[2].value, RefValue::Val2(oid3, oid1));
4810 }
4811
4812 #[test]
4813 fn test_symref_roundtrip() {
4814 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
4815 writer
4816 .add_ref(RefRecord {
4817 name: "refs/heads/sym".to_owned(),
4818 update_index: 1,
4819 value: RefValue::Symref("refs/heads/main".to_owned()),
4820 })
4821 .unwrap();
4822 let data = writer.finish().unwrap();
4823
4824 let reader = ReftableReader::new(data).unwrap();
4825 let refs = reader.read_refs().unwrap();
4826 assert_eq!(refs.len(), 1);
4827 assert_eq!(
4828 refs[0].value,
4829 RefValue::Symref("refs/heads/main".to_owned())
4830 );
4831 }
4832
4833 #[test]
4834 fn test_log_roundtrip() {
4835 let old_oid = ObjectId::from_bytes(&[0; 20]).unwrap();
4836 let new_oid = ObjectId::from_bytes(&[0xaa; 20]).unwrap();
4837
4838 let opts = WriteOptions {
4839 write_log: true,
4840 ..Default::default()
4841 };
4842 let mut writer = ReftableWriter::new(opts, 1, 1);
4843 writer
4844 .add_log(LogRecord {
4845 refname: "refs/heads/main".to_owned(),
4846 update_index: 1,
4847 old_id: old_oid,
4848 new_id: new_oid,
4849 name: "Test User".to_owned(),
4850 email: "test@example.com".to_owned(),
4851 time_seconds: 1700000000,
4852 tz_offset: -800,
4853 message: "initial commit".to_owned(),
4854 })
4855 .unwrap();
4856 let data = writer.finish().unwrap();
4857
4858 let reader = ReftableReader::new(data).unwrap();
4859 let logs = reader.read_logs().unwrap();
4860 assert_eq!(logs.len(), 1);
4861 assert_eq!(logs[0].refname, "refs/heads/main");
4862 assert_eq!(logs[0].old_id, old_oid);
4863 assert_eq!(logs[0].new_id, new_oid);
4864 assert_eq!(logs[0].name, "Test User");
4865 assert_eq!(logs[0].email, "test@example.com");
4866 assert_eq!(logs[0].time_seconds, 1700000000);
4867 assert_eq!(logs[0].tz_offset, -800);
4868 assert_eq!(logs[0].message, "initial commit\n");
4872 }
4873
4874 #[test]
4875 fn test_unaligned_table() {
4876 let oid = ObjectId::from_bytes(&[0xcc; 20]).unwrap();
4877 let opts = WriteOptions {
4878 unpadded: true,
4883 restart_interval: 16,
4884 write_log: false,
4885 ..WriteOptions::default()
4886 };
4887 let mut writer = ReftableWriter::new(opts, 1, 1);
4888 writer
4889 .add_ref(RefRecord {
4890 name: "refs/heads/main".to_owned(),
4891 update_index: 1,
4892 value: RefValue::Val1(oid),
4893 })
4894 .unwrap();
4895 let data = writer.finish().unwrap();
4896
4897 assert!(data.len() < DEFAULT_BLOCK_SIZE as usize);
4899
4900 let reader = ReftableReader::new(data).unwrap();
4901 let refs = reader.read_refs().unwrap();
4902 assert_eq!(refs.len(), 1);
4903 assert_eq!(refs[0].value, RefValue::Val1(oid));
4904 }
4905
4906 #[test]
4907 fn test_parse_identity() {
4908 let (name, email, ts, tz) =
4909 parse_identity_string("Test User <test@example.com> 1700000000 -0800");
4910 assert_eq!(name, "Test User");
4911 assert_eq!(email, "test@example.com");
4912 assert_eq!(ts, 1700000000);
4913 assert_eq!(tz, -800);
4915 let (_, _, _, tz) = parse_identity_string("Test User <test@example.com> 1700000000 +0530");
4916 assert_eq!(tz, 530);
4917 }
4918
4919 #[test]
4920 fn test_deletion_record() {
4921 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
4922 writer
4923 .add_ref(RefRecord {
4924 name: "refs/heads/gone".to_owned(),
4925 update_index: 1,
4926 value: RefValue::Deletion,
4927 })
4928 .unwrap();
4929 let data = writer.finish().unwrap();
4930
4931 let reader = ReftableReader::new(data).unwrap();
4932 let refs = reader.read_refs().unwrap();
4933 assert_eq!(refs.len(), 1);
4934 assert_eq!(refs[0].value, RefValue::Deletion);
4935 }
4936
4937 #[test]
4938 fn verify_batch_reftable_symref_ancestor_blocks_child_create() {
4939 use crate::objects::ObjectId;
4940 use crate::refs::{verify_ref_transaction_batch, RefBatchItem, RefnameUnavailable};
4941 use crate::repo::init_repository;
4942
4943 let dir = tempfile::tempdir().expect("tempdir");
4944 init_repository(
4945 dir.path(),
4946 false,
4947 "main",
4948 None,
4949 crate::RefStorageFormat::Reftable,
4950 )
4951 .expect("init");
4952 let git_dir = dir.path().join(".git");
4953 reftable_write_symref(
4954 &git_dir,
4955 "refs/heads/parent",
4956 "refs/heads/missing-target",
4957 None,
4958 None,
4959 )
4960 .expect("symref parent");
4961
4962 let child_oid = ObjectId::from_bytes(&[0x44; 20]).expect("oid");
4963 let items = vec![RefBatchItem {
4964 name: "refs/heads/parent/child".to_owned(),
4965 new_oid: Some(child_oid),
4966 }];
4967 let err = verify_ref_transaction_batch(&git_dir, &items).unwrap_err();
4968 assert!(
4969 matches!(
4970 err,
4971 RefnameUnavailable::AncestorExists {
4972 ref blocking,
4973 ref new_ref,
4974 } if blocking == "refs/heads/parent" && new_ref == "refs/heads/parent/child"
4975 ),
4976 "unexpected {err:?}"
4977 );
4978 }
4979
4980 #[test]
4981 fn update_refs_reftable_symref_ancestor_blocks_child_create() {
4982 use crate::error::Error;
4983 use crate::refs::gc::{update_refs, RefTransactionItem};
4984 use crate::refs::store::RefStoreError;
4985 use crate::refs::RefnameUnavailable;
4986 use crate::repo::init_repository;
4987
4988 let dir = tempfile::tempdir().expect("tempdir");
4989 init_repository(
4990 dir.path(),
4991 false,
4992 "main",
4993 None,
4994 crate::RefStorageFormat::Reftable,
4995 )
4996 .expect("init");
4997 let git_dir = dir.path().join(".git");
4998 reftable_write_symref(
4999 &git_dir,
5000 "refs/heads/parent",
5001 "refs/heads/missing-target",
5002 None,
5003 None,
5004 )
5005 .expect("symref parent");
5006
5007 let child_oid = crate::refs::test_commit(&git_dir, "child");
5008 let err = update_refs(
5009 &git_dir,
5010 &[RefTransactionItem {
5011 name: "refs/heads/parent/child".to_owned(),
5012 new_oid: Some(child_oid),
5013 expected_old: None,
5014 }],
5015 )
5016 .unwrap_err();
5017 match err {
5018 Error::RefStore(RefStoreError::NameUnavailable {
5019 reason: RefnameUnavailable::AncestorExists { blocking, new_ref },
5020 }) => {
5021 assert_eq!(blocking, "refs/heads/parent");
5022 assert_eq!(new_ref, "refs/heads/parent/child");
5023 }
5024 other => panic!("unexpected {other:?}"),
5025 }
5026 }
5027
5028 #[test]
5029 fn write_transaction_inject_failure_leaves_tables_list_unchanged() {
5030 use crate::objects::ObjectId;
5031 use crate::repo::init_repository;
5032 use tempfile::TempDir;
5033
5034 let tmp = TempDir::new().unwrap();
5035 let repo = init_repository(
5036 tmp.path(),
5037 false,
5038 "main",
5039 None,
5040 crate::RefStorageFormat::Reftable,
5041 )
5042 .unwrap();
5043 let git_dir = repo.git_dir();
5044 let tables_list = git_dir.join("reftable/tables.list");
5045 let before = fs::read_to_string(&tables_list).unwrap_or_default();
5046
5047 let oid = ObjectId::from_bytes(&[0xcd; 20]).unwrap();
5048 set_test_inject_reftable_transaction_fail(true);
5049 let mut stack = ReftableStack::open(&git_dir).unwrap();
5050 let opts = read_write_options(&git_dir);
5051 let err = stack.write_transaction(
5052 vec![ReftableTransactionUpdate {
5053 refname: "refs/heads/inject-fail".to_owned(),
5054 value: Some(RefValue::Val1(oid)),
5055 log: None,
5056 expected_old: None,
5057 }],
5058 &opts,
5059 );
5060 set_test_inject_reftable_transaction_fail(false);
5061 assert!(err.is_err());
5062
5063 let after = fs::read_to_string(&tables_list).unwrap_or_default();
5064 assert_eq!(after, before);
5065 }
5066
5067 #[test]
5068 fn truncated_varint_is_typed() {
5069 let err = get_varint(&[0x80], 0).expect_err("continuation byte without a successor");
5070 assert!(matches!(
5071 err,
5072 Error::Reftable(ReftableError::Truncated {
5073 field: ReftableField::Varint
5074 })
5075 ));
5076 assert!(matches!(
5077 get_varint(&[], 0),
5078 Err(Error::Reftable(ReftableError::Truncated {
5079 field: ReftableField::Varint
5080 }))
5081 ));
5082 }
5083
5084 #[test]
5085 fn writer_rejects_unsorted_refs_with_both_names() {
5086 let oid = ObjectId::from_bytes(&[0x11; 20]).expect("oid");
5087 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
5088 writer
5089 .add_ref(RefRecord {
5090 name: "refs/heads/b".to_owned(),
5091 update_index: 1,
5092 value: RefValue::Val1(oid),
5093 })
5094 .expect("first ref");
5095 let err = writer
5096 .add_ref(RefRecord {
5097 name: "refs/heads/a".to_owned(),
5098 update_index: 1,
5099 value: RefValue::Val1(oid),
5100 })
5101 .expect_err("out of order");
5102 match err {
5103 Error::Reftable(ReftableError::UnsortedRefs { previous, next }) => {
5104 assert_eq!(previous, "refs/heads/b");
5105 assert_eq!(next, "refs/heads/a");
5106 }
5107 other => panic!("unexpected {other:?}"),
5108 }
5109 }
5110
5111 #[test]
5112 fn reader_reports_footer_version_mismatch_and_short_files() {
5113 let data = ReftableWriter::new(WriteOptions::default(), 1, 1)
5114 .finish()
5115 .expect("empty table");
5116 let footer_start = data.len() - FOOTER_V1_SIZE;
5117
5118 let mut mismatched = data.clone();
5119 mismatched[footer_start + 4] = 2;
5120 assert!(matches!(
5121 ReftableReader::new(mismatched),
5122 Err(Error::Reftable(ReftableError::FooterVersionMismatch {
5123 header: 1,
5124 footer: 2
5125 }))
5126 ));
5127
5128 assert!(matches!(
5129 ReftableReader::new(data[..10].to_vec()),
5130 Err(Error::Reftable(ReftableError::TooSmall { len: 10 }))
5131 ));
5132 assert!(matches!(
5133 ReftableReader::new(data[..HEADER_SIZE + 4].to_vec()),
5134 Err(Error::Reftable(ReftableError::TooSmall { .. }))
5135 ));
5136 }
5137
5138 #[test]
5139 fn write_transaction_reports_expected_mismatch() {
5140 use crate::repo::init_repository;
5141
5142 let tmp = tempfile::tempdir().expect("tempdir");
5143 let repo = init_repository(
5144 tmp.path(),
5145 false,
5146 "main",
5147 None,
5148 crate::RefStorageFormat::Reftable,
5149 )
5150 .expect("init");
5151 let git_dir = repo.git_dir();
5152 let opts = read_write_options(&git_dir);
5153 let current = ObjectId::from_bytes(&[0x21; 20]).expect("oid");
5154 let stale = ObjectId::from_bytes(&[0x22; 20]).expect("oid");
5155 let mut stack = ReftableStack::open(&git_dir).expect("open");
5156 stack
5157 .write_transaction(
5158 vec![ReftableTransactionUpdate {
5159 refname: "refs/heads/topic".to_owned(),
5160 value: Some(RefValue::Val1(current)),
5161 log: None,
5162 expected_old: None,
5163 }],
5164 &opts,
5165 )
5166 .expect("seed topic");
5167
5168 let update = |refname: &str| ReftableTransactionUpdate {
5169 refname: refname.to_owned(),
5170 value: Some(RefValue::Val1(stale)),
5171 log: None,
5172 expected_old: Some(stale),
5173 };
5174 match stack.write_transaction(vec![update("refs/heads/topic")], &opts) {
5175 Err(Error::Reftable(ReftableError::ExpectedMismatch {
5176 refname,
5177 expected,
5178 actual,
5179 })) => {
5180 assert_eq!(refname, "refs/heads/topic");
5181 assert_eq!(expected, stale);
5182 assert_eq!(actual, Some(current));
5183 }
5184 other => panic!("unexpected {other:?}"),
5185 }
5186 let err = stack
5187 .write_transaction(vec![update("refs/heads/absent")], &opts)
5188 .expect_err("absent ref");
5189 assert!(matches!(
5190 &err,
5191 Error::Reftable(ReftableError::ExpectedMismatch { actual: None, .. })
5192 ));
5193 assert!(err.to_string().ends_with("but found <absent>"), "{err}");
5194 }
5195
5196 #[test]
5197 fn error_display_names_the_field() {
5198 assert_eq!(
5199 ReftableError::Truncated {
5200 field: ReftableField::LogEmail
5201 }
5202 .to_string(),
5203 "reftable: truncated log email"
5204 );
5205 assert_eq!(
5206 Error::from(ReftableError::InvalidUtf8 {
5207 field: ReftableField::SymrefTarget
5208 })
5209 .to_string(),
5210 "reftable: symref target is not valid UTF-8"
5211 );
5212 }
5213
5214 #[test]
5215 fn write_table_file_retries_when_destination_races_before_persist() {
5216 use crate::repo::init_repository;
5217
5218 let dir = tempfile::tempdir().expect("tempdir");
5219 init_repository(
5220 dir.path(),
5221 false,
5222 "main",
5223 None,
5224 crate::RefStorageFormat::Reftable,
5225 )
5226 .expect("init");
5227 let git_dir = dir.path().join(".git");
5228 let stack = ReftableStack::open(&git_dir).expect("open");
5229 let reftable_dir = git_dir.join("reftable");
5230 let data = ReftableWriter::new(WriteOptions::default(), 1, 1)
5231 .finish()
5232 .expect("empty table");
5233 let colliding = 0x1234_5678u32;
5234 let fallback = 0x8765_4321u32;
5235 let raced_name = reftable_table_filename(1, 1, colliding);
5236 struct ClearPersistHook;
5237 impl Drop for ClearPersistHook {
5238 fn drop(&mut self) {
5239 reftable_write_test_hooks::set_before_persist_hook(None);
5240 }
5241 }
5242 let _clear_hook = ClearPersistHook;
5243 reftable_write_test_hooks::set_before_persist_hook(Some(Box::new({
5244 let raced_name = raced_name.clone();
5245 move |path| {
5246 if path.file_name().and_then(|n| n.to_str()) == Some(raced_name.as_str()) {
5247 fs::write(path, b"raced-in").expect("race destination into place");
5248 }
5249 }
5250 })));
5251 reftable_write_test_hooks::push_table_suffixes([colliding, fallback]);
5252 let published = stack
5253 .write_table_file(&data, 1, 1)
5254 .expect("publish with new suffix after race");
5255 assert_eq!(published, reftable_table_filename(1, 1, fallback));
5256 assert_eq!(
5257 fs::read(reftable_dir.join(&raced_name)).expect("read raced file"),
5258 b"raced-in"
5259 );
5260 let leftovers: Vec<_> = fs::read_dir(&reftable_dir)
5261 .expect("read reftable dir")
5262 .flatten()
5263 .map(|e| e.file_name())
5264 .filter(|name| {
5265 name.to_str()
5266 .is_some_and(|s| s.starts_with(".writ-") && s.ends_with(".tmp"))
5267 })
5268 .collect();
5269 assert!(
5270 leftovers.is_empty(),
5271 "stale suffix reservation temps: {leftovers:?}"
5272 );
5273 }
5274
5275 #[test]
5276 fn write_table_file_does_not_clobber_existing_destination() {
5277 use crate::repo::init_repository;
5278
5279 let dir = tempfile::tempdir().expect("tempdir");
5280 init_repository(
5281 dir.path(),
5282 false,
5283 "main",
5284 None,
5285 crate::RefStorageFormat::Reftable,
5286 )
5287 .expect("init");
5288 let git_dir = dir.path().join(".git");
5289 let stack = ReftableStack::open(&git_dir).expect("open");
5290 let reftable_dir = git_dir.join("reftable");
5291 let data = ReftableWriter::new(WriteOptions::default(), 1, 1)
5292 .finish()
5293 .expect("empty table");
5294 let colliding = 0x1234_5678u32;
5295 let fallback = 0x8765_4321u32;
5296 let blocked = reftable_table_filename(1, 1, colliding);
5297 fs::write(reftable_dir.join(&blocked), b"keep").expect("seed existing table");
5298 reftable_write_test_hooks::push_table_suffixes([colliding, fallback]);
5299 let published = stack
5300 .write_table_file(&data, 1, 1)
5301 .expect("publish with new suffix");
5302 assert_eq!(published, reftable_table_filename(1, 1, fallback));
5303 assert_eq!(
5304 fs::read(reftable_dir.join(&blocked)).expect("read blocked file"),
5305 b"keep"
5306 );
5307 }
5308
5309 #[test]
5310 fn write_table_file_uses_distinct_suffixes() {
5311 use crate::repo::init_repository;
5312
5313 let dir = tempfile::tempdir().expect("tempdir");
5314 init_repository(
5315 dir.path(),
5316 false,
5317 "main",
5318 None,
5319 crate::RefStorageFormat::Reftable,
5320 )
5321 .expect("init");
5322 let git_dir = dir.path().join(".git");
5323 let stack = ReftableStack::open(&git_dir).expect("open");
5324 let data = ReftableWriter::new(WriteOptions::default(), 1, 1)
5325 .finish()
5326 .expect("empty table");
5327 let first = stack
5328 .write_table_file(&data, 1, 1)
5329 .expect("first table file");
5330 let second = stack
5331 .write_table_file(&data, 1, 1)
5332 .expect("second table file");
5333 assert_ne!(first, second);
5334 }
5335
5336 #[test]
5337 fn concurrent_readers_survive_autocompact_writes() {
5338 use crate::objects::ObjectId;
5339 use crate::repo::init_repository;
5340 use std::sync::atomic::{AtomicBool, Ordering};
5341 use std::sync::Arc;
5342 use std::thread;
5343 use std::time::Duration;
5344
5345 let dir = tempfile::tempdir().expect("tempdir");
5346 init_repository(
5347 dir.path(),
5348 false,
5349 "main",
5350 None,
5351 crate::RefStorageFormat::Reftable,
5352 )
5353 .expect("init");
5354 let git_dir = dir.path().join(".git");
5355 let opts = read_write_options(&git_dir);
5356 let stop = Arc::new(AtomicBool::new(false));
5357
5358 let git_dir_writer = git_dir.clone();
5359 let opts_writer = opts.clone();
5360 let stop_writer = Arc::clone(&stop);
5361 let writer = thread::spawn(move || {
5362 let mut stack = ReftableStack::open(&git_dir_writer).expect("writer open");
5363 for i in 0..120u8 {
5364 let oid = ObjectId::from_bytes(&[i; 20]).expect("oid");
5365 stack
5366 .write_ref(
5367 &format!("refs/heads/stress/{i}"),
5368 RefValue::Val1(oid),
5369 None,
5370 &opts_writer,
5371 )
5372 .expect("write_ref");
5373 }
5374 stop_writer.store(true, Ordering::Release);
5375 });
5376
5377 let mut readers = Vec::new();
5378 for _ in 0..4 {
5379 let git_dir_reader = git_dir.clone();
5380 let stop_reader = Arc::clone(&stop);
5381 readers.push(thread::spawn(move || {
5382 let stack = ReftableStack::open(&git_dir_reader).expect("reader open");
5383 while !stop_reader.load(Ordering::Acquire) {
5384 stack.read_refs().expect("read_refs");
5385 stack
5386 .list_refs_under_prefix(&git_dir_reader, "refs/heads/stress/")
5387 .expect("list refs");
5388 thread::sleep(Duration::from_millis(1));
5389 }
5390 for _ in 0..30 {
5391 stack.read_refs().expect("read_refs tail");
5392 stack.read_all_logs().expect("read_all_logs tail");
5393 }
5394 }));
5395 }
5396
5397 writer.join().expect("writer join");
5398 for reader in readers {
5399 reader.join().expect("reader join");
5400 }
5401
5402 let refs = ReftableStack::open(&git_dir)
5403 .expect("final open")
5404 .read_refs()
5405 .expect("final read");
5406 assert!(
5407 refs.iter()
5408 .filter(|r| r.name.starts_with("refs/heads/stress/"))
5409 .count()
5410 >= 120,
5411 "expected every stress ref to survive concurrent autocompaction"
5412 );
5413 }
5414}