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