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