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