1use crate::config::ConfigSet;
29use crate::error::{Error, Result};
30use crate::objects::ObjectId;
31use crate::refs::store::RefStoreError;
32use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
33use std::fs;
34use std::io::{Read, Write};
35use std::ops::ControlFlow;
36use std::path::{Path, PathBuf};
37use std::sync::{Arc, Mutex, OnceLock};
38
39const REFTABLE_MAGIC: &[u8; 4] = b"REFT";
45
46const HEADER_SIZE: usize = 24;
49
50const FOOTER_V1_SIZE: usize = 68;
52
53const BLOCK_TYPE_REF: u8 = b'r';
55const BLOCK_TYPE_INDEX: u8 = b'i';
57const BLOCK_TYPE_LOG: u8 = b'g';
59const BLOCK_TYPE_OBJ: u8 = b'o';
61
62const VALUE_DELETION: u8 = 0;
64const VALUE_ONE_OID: u8 = 1;
65const VALUE_TWO_OID: u8 = 2;
66const VALUE_SYMREF: u8 = 3;
67
68const HASH_SIZE: usize = 20;
70
71const DEFAULT_BLOCK_SIZE: u32 = 4096;
73
74const RESTART_INTERVAL: usize = 16;
76
77#[derive(Debug, thiserror::Error)]
83#[non_exhaustive]
84pub enum ReftableError {
85 #[error("reftable: file is too small ({len} bytes)")]
87 TooSmall {
88 len: usize,
90 },
91
92 #[error("reftable: bad magic bytes")]
94 BadMagic,
95
96 #[error("reftable: unsupported format version {version}")]
98 UnsupportedVersion {
99 version: u8,
101 },
102
103 #[error("reftable: footer version {footer} does not match header version {header}")]
105 FooterVersionMismatch {
106 header: u8,
108 footer: u8,
110 },
111
112 #[error("reftable: footer checksum mismatch (stored {stored:08x}, computed {computed:08x})")]
114 FooterChecksumMismatch {
115 stored: u32,
117 computed: u32,
119 },
120
121 #[error("reftable: truncated {field}")]
123 Truncated {
124 field: ReftableField,
126 },
127
128 #[error("reftable: {field} is not valid UTF-8")]
130 InvalidUtf8 {
131 field: ReftableField,
133 },
134
135 #[error("reftable: {field} prefix is longer than the previous key")]
137 PrefixTooLong {
138 field: ReftableField,
140 },
141
142 #[error("reftable: unknown ref value type {value_type}")]
144 UnknownValueType {
145 value_type: u8,
147 },
148
149 #[error("reftable: invalid block length")]
151 InvalidBlockLength,
152
153 #[error("reftable: invalid restart table")]
155 InvalidRestartTable,
156
157 #[error("reftable: malformed log record key")]
159 InvalidLogKey,
160
161 #[error("reftable: tables.list names missing table {name}")]
163 MissingTable {
164 name: String,
166 },
167
168 #[error("reftable: refs must be added in sorted order ('{next}' after '{previous}')")]
170 UnsortedRefs {
171 previous: String,
173 next: String,
175 },
176
177 #[error("reftable: record does not fit in a block")]
179 EntryTooLarge,
180
181 #[error("reftable: record has an empty key")]
183 EmptyRecordKey,
184
185 #[error("reftable: stack is locked by another writer ({})", lock_path.display())]
187 Locked {
188 lock_path: PathBuf,
190 },
191
192 #[error("reftable: tables.list lock was already released")]
194 LockReleased,
195
196 #[error(
198 "reftable: '{refname}' expected {expected} but found {}",
199 actual.map_or_else(|| "<absent>".to_owned(), |oid| oid.to_hex())
200 )]
201 ExpectedMismatch {
202 refname: String,
204 expected: ObjectId,
206 actual: Option<ObjectId>,
208 },
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213#[non_exhaustive]
214pub enum ReftableField {
215 Varint,
217 BlockHeader,
219 IndexKey,
221 RefName,
223 ObjectId,
225 ObjectIdPair,
227 SymrefTarget,
229 LogKey,
231 LogName,
233 LogEmail,
235 LogTimezone,
237 LogMessage,
239 Footer,
241}
242
243impl std::fmt::Display for ReftableField {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 f.write_str(match self {
246 Self::Varint => "varint",
247 Self::BlockHeader => "block header",
248 Self::IndexKey => "index key",
249 Self::RefName => "ref name",
250 Self::ObjectId => "object id",
251 Self::ObjectIdPair => "object id pair",
252 Self::SymrefTarget => "symref target",
253 Self::LogKey => "log key",
254 Self::LogName => "log name",
255 Self::LogEmail => "log email",
256 Self::LogTimezone => "log timezone offset",
257 Self::LogMessage => "log message",
258 Self::Footer => "footer",
259 })
260 }
261}
262
263#[cold]
264#[inline(never)]
265fn truncated(field: ReftableField) -> Error {
266 ReftableError::Truncated { field }.into()
267}
268
269#[cold]
270#[inline(never)]
271fn invalid_utf8(field: ReftableField) -> Error {
272 ReftableError::InvalidUtf8 { field }.into()
273}
274
275#[cold]
276#[inline(never)]
277fn corrupt(err: ReftableError) -> Error {
278 err.into()
279}
280
281fn put_varint(mut val: u64, out: &mut Vec<u8>) -> usize {
287 let mut buf = [0u8; 10];
289 let mut i = 0;
290 buf[i] = (val & 0x7f) as u8;
291 i += 1;
292 val >>= 7;
293 while val > 0 {
294 val -= 1;
295 buf[i] = (val & 0x7f) as u8;
296 i += 1;
297 val >>= 7;
298 }
299 let len = i;
301 for j in (1..len).rev() {
302 out.push(buf[j] | 0x80);
303 }
304 out.push(buf[0]);
305 len
306}
307
308fn get_varint(data: &[u8], mut pos: usize) -> Result<(u64, usize)> {
310 if pos >= data.len() {
311 return Err(truncated(ReftableField::Varint));
312 }
313 let mut val = (data[pos] & 0x7f) as u64;
314 while data[pos] & 0x80 != 0 {
315 pos += 1;
316 if pos >= data.len() {
317 return Err(truncated(ReftableField::Varint));
318 }
319 val = ((val + 1) << 7) | (data[pos] & 0x7f) as u64;
320 }
321 Ok((val, pos + 1))
322}
323
324#[derive(Debug, Clone, PartialEq, Eq)]
330pub enum RefValue {
331 Deletion,
333 Val1(ObjectId),
335 Val2(ObjectId, ObjectId),
337 Symref(String),
339}
340
341#[derive(Debug, Clone)]
343pub struct RefRecord {
344 pub name: String,
346 pub update_index: u64,
348 pub value: RefValue,
350}
351
352#[derive(Debug, Clone)]
354pub struct LogRecord {
355 pub refname: String,
357 pub update_index: u64,
359 pub old_id: ObjectId,
361 pub new_id: ObjectId,
363 pub name: String,
365 pub email: String,
367 pub time_seconds: u64,
369 pub tz_offset: i16,
372 pub message: String,
374}
375
376#[derive(Debug, Clone)]
378pub struct WriteOptions {
379 pub block_size: u32,
381 pub restart_interval: usize,
383 pub write_log: bool,
385 pub skip_index_objects: bool,
387 pub unpadded: bool,
389 pub hash_size: usize,
392 pub auto_compaction_factor: u8,
395 pub autocompaction_enabled: bool,
398}
399
400impl Default for WriteOptions {
401 fn default() -> Self {
402 Self {
403 block_size: DEFAULT_BLOCK_SIZE,
404 restart_interval: RESTART_INTERVAL,
405 write_log: true,
406 skip_index_objects: false,
407 unpadded: false,
408 hash_size: HASH_SIZE,
409 auto_compaction_factor: 2,
410 autocompaction_enabled: true,
411 }
412 }
413}
414
415#[derive(Debug, Clone)]
422pub struct ReftableTransactionUpdate {
423 pub refname: String,
425 pub value: Option<RefValue>,
427 pub log: Option<LogRecord>,
429 pub expected_old: Option<ObjectId>,
432}
433
434pub struct ReftableWriter {
448 opts: WriteOptions,
449 min_update_index: u64,
450 max_update_index: u64,
451
452 refs: Vec<RefRecord>,
454 logs: Vec<LogRecord>,
456}
457
458impl ReftableWriter {
459 pub fn new(opts: WriteOptions, min_update_index: u64, max_update_index: u64) -> Self {
461 Self {
462 opts,
463 min_update_index,
464 max_update_index,
465 refs: Vec::new(),
466 logs: Vec::new(),
467 }
468 }
469
470 pub fn add_ref(&mut self, rec: RefRecord) -> Result<()> {
472 if let Some(last) = self.refs.last() {
473 if rec.name <= last.name {
474 return Err(ReftableError::UnsortedRefs {
475 previous: last.name.clone(),
476 next: rec.name,
477 }
478 .into());
479 }
480 }
481 self.refs.push(rec);
482 Ok(())
483 }
484
485 pub fn add_log(&mut self, rec: LogRecord) -> Result<()> {
487 self.logs.push(rec);
488 Ok(())
489 }
490
491 pub fn finish(self) -> Result<Vec<u8>> {
497 let refs = self.refs;
498 let logs = self.logs;
499 let opts = self.opts;
500 let mut w = WriterState::new(opts, self.min_update_index, self.max_update_index);
501
502 for rec in &refs {
504 w.add_ref(rec)?;
505 }
506
507 let mut logs = logs;
510 logs.sort_by(|a, b| {
511 a.refname
512 .cmp(&b.refname)
513 .then_with(|| b.update_index.cmp(&a.update_index))
514 });
515 if w.opts.write_log {
516 for log in &logs {
517 w.add_log(log)?;
518 }
519 }
520
521 w.close()
522 }
523}
524
525const REFTABLE_DEFAULT_BLOCK_SIZE: u32 = 4096;
531const MAX_RESTARTS: usize = (1 << 16) - 1;
533
534enum EncRecord<'a> {
536 Ref(&'a RefRecord, u64),
537 Log(&'a LogRecord),
538 Obj { prefix: Vec<u8>, offsets: Vec<u64> },
539 Index { last_key: Vec<u8>, offset: u64 },
540}
541
542impl EncRecord<'_> {
543 fn block_type(&self) -> u8 {
544 match self {
545 EncRecord::Ref(..) => BLOCK_TYPE_REF,
546 EncRecord::Log(_) => BLOCK_TYPE_LOG,
547 EncRecord::Obj { .. } => BLOCK_TYPE_OBJ,
548 EncRecord::Index { .. } => BLOCK_TYPE_INDEX,
549 }
550 }
551
552 fn key(&self) -> Vec<u8> {
554 match self {
555 EncRecord::Ref(r, _) => r.name.as_bytes().to_vec(),
556 EncRecord::Log(l) => {
557 let mut k = Vec::with_capacity(l.refname.len() + 9);
558 k.extend_from_slice(l.refname.as_bytes());
559 k.push(0);
560 let ts = u64::MAX - l.update_index;
561 k.extend_from_slice(&ts.to_be_bytes());
562 k
563 }
564 EncRecord::Obj { prefix, .. } => prefix.clone(),
565 EncRecord::Index { last_key, .. } => last_key.clone(),
566 }
567 }
568
569 fn val_type(&self) -> u8 {
571 match self {
572 EncRecord::Ref(r, _) => match r.value {
573 RefValue::Deletion => VALUE_DELETION,
574 RefValue::Val1(_) => VALUE_ONE_OID,
575 RefValue::Val2(..) => VALUE_TWO_OID,
576 RefValue::Symref(_) => VALUE_SYMREF,
577 },
578 EncRecord::Log(_) => 1,
581 EncRecord::Obj { offsets, .. } => {
582 if !offsets.is_empty() && offsets.len() < 8 {
583 offsets.len() as u8
584 } else {
585 0
586 }
587 }
588 EncRecord::Index { .. } => 0,
589 }
590 }
591
592 fn encode_value(&self, opts: &WriteOptions, out: &mut Vec<u8>) {
594 match self {
595 EncRecord::Ref(r, update_index_delta) => {
596 put_varint(*update_index_delta, out);
597 match &r.value {
598 RefValue::Deletion => {}
599 RefValue::Val1(oid) => out.extend_from_slice(oid.as_bytes()),
600 RefValue::Val2(oid, peeled) => {
601 out.extend_from_slice(oid.as_bytes());
602 out.extend_from_slice(peeled.as_bytes());
603 }
604 RefValue::Symref(target) => {
605 put_varint(target.len() as u64, out);
606 out.extend_from_slice(target.as_bytes());
607 }
608 }
609 }
610 EncRecord::Log(l) => {
611 out.extend_from_slice(l.old_id.as_bytes());
612 out.extend_from_slice(l.new_id.as_bytes());
613 put_varint(l.name.len() as u64, out);
614 out.extend_from_slice(l.name.as_bytes());
615 put_varint(l.email.len() as u64, out);
616 out.extend_from_slice(l.email.as_bytes());
617 put_varint(l.time_seconds, out);
618 out.extend_from_slice(&l.tz_offset.to_be_bytes());
619 let msg = clean_log_message(&l.message, opts);
620 put_varint(msg.len() as u64, out);
621 out.extend_from_slice(&msg);
622 }
623 EncRecord::Obj { offsets, .. } => {
624 if offsets.is_empty() || offsets.len() >= 8 {
625 put_varint(offsets.len() as u64, out);
626 }
627 if offsets.is_empty() {
628 return;
629 }
630 put_varint(offsets[0], out);
631 let mut last = offsets[0];
632 for &o in &offsets[1..] {
633 put_varint(o - last, out);
634 last = o;
635 }
636 }
637 EncRecord::Index { offset, .. } => {
638 put_varint(*offset, out);
639 }
640 }
641 }
642}
643
644fn clean_log_message(msg: &str, opts: &WriteOptions) -> Vec<u8> {
655 let limit = (opts.block_size as usize / 2).max(1);
662 let msg = if msg.len() > limit {
663 let mut end = limit;
664 while end > 0 && !msg.is_char_boundary(end) {
665 end -= 1;
666 }
667 &msg[..end]
668 } else {
669 msg
670 };
671 let trimmed = msg.trim_end_matches('\n');
672 let mut out = trimmed.as_bytes().to_vec();
673 out.push(b'\n');
674 out
675}
676
677fn encode_key(prev: &[u8], key: &[u8], extra: u8, out: &mut Vec<u8>) -> bool {
680 let prefix_len = common_prefix_len(prev, key);
681 let suffix_len = key.len() - prefix_len;
682 put_varint(prefix_len as u64, out);
683 put_varint(((suffix_len as u64) << 3) | (extra as u64), out);
684 out.extend_from_slice(&key[prefix_len..]);
685 prefix_len == 0
686}
687
688struct BlockWriter {
690 typ: u8,
691 buf: Vec<u8>,
694 header_off: usize,
695 block_size: usize,
696 restart_interval: usize,
697 restarts: Vec<u32>,
698 last_key: Vec<u8>,
699 entries: usize,
700}
701
702impl BlockWriter {
703 fn new(typ: u8, block_size: usize, header_off: usize, restart_interval: usize) -> Self {
704 let mut buf = vec![0u8; header_off + 4];
706 buf[header_off] = typ;
707 Self {
708 typ,
709 buf,
710 header_off,
711 block_size,
712 restart_interval,
713 restarts: Vec::new(),
714 last_key: Vec::new(),
715 entries: 0,
716 }
717 }
718
719 fn next(&self) -> usize {
722 self.buf.len()
723 }
724
725 fn add(&mut self, rec: &EncRecord, opts: &WriteOptions) -> Result<bool> {
728 let key = rec.key();
729 if key.is_empty() {
730 return Err(ReftableError::EmptyRecordKey.into());
731 }
732 let restart = self.entries.is_multiple_of(self.restart_interval);
733 let prev: &[u8] = if restart { &[] } else { &self.last_key };
734
735 let mut encoded = Vec::new();
736 let is_restart = encode_key(prev, &key, rec.val_type(), &mut encoded);
737 rec.encode_value(opts, &mut encoded);
738 let n = encoded.len();
739
740 let mut rlen = self.restarts.len();
742 let mut is_restart = is_restart;
743 if rlen >= MAX_RESTARTS {
744 is_restart = false;
745 }
746 if is_restart {
747 rlen += 1;
748 }
749 if self.block_size > 0 && 2 + 3 * rlen + n > self.block_size - self.next() {
750 return Ok(false);
751 }
752
753 if is_restart {
754 self.restarts.push(self.next() as u32);
755 }
756 self.buf.extend_from_slice(&encoded);
757 self.last_key = key;
758 self.entries += 1;
759 Ok(true)
760 }
761
762 fn finish(&mut self) -> Result<usize> {
766 for &r in &self.restarts {
767 self.buf.push(((r >> 16) & 0xff) as u8);
768 self.buf.push(((r >> 8) & 0xff) as u8);
769 self.buf.push((r & 0xff) as u8);
770 }
771 let rc = self.restarts.len() as u16;
772 self.buf.push((rc >> 8) as u8);
773 self.buf.push((rc & 0xff) as u8);
774
775 let block_len = self.buf.len();
777 self.buf[self.header_off + 1] = ((block_len >> 16) & 0xff) as u8;
778 self.buf[self.header_off + 2] = ((block_len >> 8) & 0xff) as u8;
779 self.buf[self.header_off + 3] = (block_len & 0xff) as u8;
780
781 if self.typ == BLOCK_TYPE_LOG {
782 use flate2::write::ZlibEncoder;
783 use flate2::Compression;
784 let skip = 4 + self.header_off;
785 let mut enc = ZlibEncoder::new(Vec::new(), Compression::new(9));
786 enc.write_all(&self.buf[skip..])
787 .map_err(|e| Error::Zlib(e.to_string()))?;
788 let compressed = enc.finish().map_err(|e| Error::Zlib(e.to_string()))?;
789 self.buf.truncate(skip);
790 self.buf.extend_from_slice(&compressed);
791 }
792 Ok(self.buf.len())
793 }
794}
795
796#[derive(Default, Clone)]
798struct SectionStats {
799 blocks: usize,
800 index_blocks: usize,
801 offset: u64,
802 index_offset: u64,
803}
804
805struct ObjEntry {
807 hash: Vec<u8>,
808 offsets: Vec<u64>,
809}
810
811struct WriterState {
813 opts: WriteOptions,
814 min_update_index: u64,
815 max_update_index: u64,
816
817 out: Vec<u8>,
818 next: u64,
819 pending_padding: usize,
820
821 block: Option<BlockWriter>,
822 block_type: u8,
823
824 index: Vec<(Vec<u8>, u64)>,
826
827 obj_entries: Vec<ObjEntry>,
829 object_id_len: usize,
830
831 ref_stats: SectionStats,
832 obj_stats: SectionStats,
833 log_stats: SectionStats,
834 idx_blocks_total: usize,
835}
836
837impl WriterState {
838 fn new(mut opts: WriteOptions, min: u64, max: u64) -> Self {
839 if opts.restart_interval == 0 {
840 opts.restart_interval = RESTART_INTERVAL;
841 }
842 if opts.block_size == 0 {
843 opts.block_size = REFTABLE_DEFAULT_BLOCK_SIZE;
844 }
845 Self {
846 opts,
847 min_update_index: min,
848 max_update_index: max,
849 out: Vec::new(),
850 next: 0,
851 pending_padding: 0,
852 block: None,
853 block_type: 0,
854 index: Vec::new(),
855 obj_entries: Vec::new(),
856 object_id_len: 0,
857 ref_stats: SectionStats::default(),
858 obj_stats: SectionStats::default(),
859 log_stats: SectionStats::default(),
860 idx_blocks_total: 0,
861 }
862 }
863
864 fn header_size(&self) -> usize {
865 if self.opts.hash_size == 32 {
868 28
869 } else {
870 24
871 }
872 }
873
874 fn write_header(&self, dest: &mut [u8]) {
875 dest[0..4].copy_from_slice(REFTABLE_MAGIC);
876 dest[4] = if self.opts.hash_size == 32 { 2 } else { 1 };
877 dest[5] = ((self.opts.block_size >> 16) & 0xff) as u8;
878 dest[6] = ((self.opts.block_size >> 8) & 0xff) as u8;
879 dest[7] = (self.opts.block_size & 0xff) as u8;
880 dest[8..16].copy_from_slice(&self.min_update_index.to_be_bytes());
881 dest[16..24].copy_from_slice(&self.max_update_index.to_be_bytes());
882 if self.opts.hash_size == 32 {
885 dest[24..28].copy_from_slice(b"s256");
886 }
887 }
888
889 fn stats_mut(&mut self, typ: u8) -> &mut SectionStats {
890 match typ {
891 BLOCK_TYPE_REF => &mut self.ref_stats,
892 BLOCK_TYPE_OBJ => &mut self.obj_stats,
893 BLOCK_TYPE_LOG => &mut self.log_stats,
894 _ => &mut self.ref_stats,
896 }
897 }
898
899 fn padded_write(&mut self, data: &[u8], padding: usize) {
902 if self.pending_padding > 0 {
903 self.out
904 .extend(std::iter::repeat_n(0u8, self.pending_padding));
905 self.pending_padding = 0;
906 }
907 self.pending_padding = padding;
908 self.out.extend_from_slice(data);
909 }
910
911 fn reinit_block(&mut self, typ: u8) {
912 let header_off = if self.next == 0 {
913 self.header_size()
914 } else {
915 0
916 };
917 self.block = Some(BlockWriter::new(
918 typ,
919 self.opts.block_size as usize,
920 header_off,
921 self.opts.restart_interval,
922 ));
923 self.block_type = typ;
924 }
925
926 fn add_record(&mut self, rec: &EncRecord) -> Result<()> {
927 let typ = rec.block_type();
928 if self.block.is_none() {
929 self.reinit_block(typ);
930 }
931 if self.add_to_block(rec)? {
932 return Ok(());
933 }
934 self.flush_block()?;
936 self.reinit_block(typ);
937 if !self.add_to_block(rec)? {
938 return Err(ReftableError::EntryTooLarge.into());
939 }
940 Ok(())
941 }
942
943 fn add_to_block(&mut self, rec: &EncRecord) -> Result<bool> {
946 match self.block.as_mut() {
947 Some(bw) => bw.add(rec, &self.opts),
948 None => Ok(false),
949 }
950 }
951
952 fn add_ref(&mut self, r: &RefRecord) -> Result<()> {
953 let delta = r.update_index.saturating_sub(self.min_update_index);
954 self.add_record(&EncRecord::Ref(r, delta))?;
955
956 if !self.opts.skip_index_objects {
957 match &r.value {
958 RefValue::Val1(oid) => self.index_hash(oid.as_bytes()),
959 RefValue::Val2(oid, peeled) => {
960 self.index_hash(oid.as_bytes());
961 self.index_hash(peeled.as_bytes());
962 }
963 _ => {}
964 }
965 }
966 Ok(())
967 }
968
969 fn add_log(&mut self, l: &LogRecord) -> Result<()> {
970 if matches!(&self.block, Some(b) if b.typ == BLOCK_TYPE_REF) {
972 self.finish_public_section()?;
973 }
974 self.next -= self.pending_padding as u64;
976 self.pending_padding = 0;
977 self.add_record(&EncRecord::Log(l))
978 }
979
980 fn index_hash(&mut self, hash: &[u8]) {
981 let off = self.next;
982 match self
983 .obj_entries
984 .binary_search_by(|e| e.hash.as_slice().cmp(hash))
985 {
986 Ok(idx) => {
987 let e = &mut self.obj_entries[idx];
988 if e.offsets.last() != Some(&off) {
989 e.offsets.push(off);
990 }
991 }
992 Err(idx) => {
993 self.obj_entries.insert(
994 idx,
995 ObjEntry {
996 hash: hash.to_vec(),
997 offsets: vec![off],
998 },
999 );
1000 }
1001 }
1002 }
1003
1004 fn flush_block(&mut self) -> Result<()> {
1006 let Some(mut bw) = self.block.take() else {
1007 return Ok(());
1008 };
1009 if bw.entries == 0 {
1010 self.block = Some(bw);
1011 return Ok(());
1012 }
1013 let typ = bw.typ;
1014 let raw_bytes = bw.finish()?;
1015
1016 let mut padding = 0;
1017 if !self.opts.unpadded && typ != BLOCK_TYPE_LOG {
1018 padding = (self.opts.block_size as usize).saturating_sub(raw_bytes);
1019 }
1020
1021 let block_typ_off = if self.stats_mut(typ).blocks == 0 {
1022 self.next
1023 } else {
1024 0
1025 };
1026 {
1027 let next = self.next;
1028 let st = self.stats_mut(typ);
1029 if block_typ_off > 0 {
1030 st.offset = next;
1031 }
1032 st.blocks += 1;
1033 }
1034
1035 if self.next == 0 {
1036 let hs = self.header_size();
1038 self.write_header_into_block(&mut bw, hs);
1039 }
1040
1041 let data = bw.buf.clone();
1042 self.padded_write(&data, padding);
1043
1044 self.index.push((bw.last_key.clone(), self.next));
1046
1047 self.next += (padding + raw_bytes) as u64;
1048 self.block = None;
1049 Ok(())
1050 }
1051
1052 fn write_header_into_block(&self, bw: &mut BlockWriter, hs: usize) {
1053 let mut hdr = vec![0u8; hs];
1054 self.write_header(&mut hdr);
1055 bw.buf[..hs].copy_from_slice(&hdr);
1056 }
1057
1058 fn flush_block_if_nonempty(&mut self) -> Result<()> {
1059 if matches!(&self.block, Some(b) if b.entries == 0) {
1060 return Ok(());
1061 }
1062 self.flush_block()
1063 }
1064
1065 fn finish_section(&mut self) -> Result<()> {
1067 let typ = self.block_type;
1068 let threshold = if self.opts.unpadded { 1 } else { 3 };
1069 let before_blocks = self.idx_blocks_total;
1070
1071 self.flush_block_if_nonempty()?;
1072
1073 let mut max_level = 0;
1074 let mut index_start = 0u64;
1075
1076 while self.index.len() > threshold {
1077 max_level += 1;
1078 index_start = self.next;
1079 self.reinit_block(BLOCK_TYPE_INDEX);
1080
1081 let idx = std::mem::take(&mut self.index);
1082 for (last_key, offset) in &idx {
1083 self.add_record(&EncRecord::Index {
1084 last_key: last_key.clone(),
1085 offset: *offset,
1086 })?;
1087 }
1088 let blocks_before = self.count_index_blocks_marker();
1090 self.flush_index_block()?;
1091 let _ = blocks_before;
1092 }
1093
1094 self.index.clear();
1095
1096 let index_blocks = self.idx_blocks_total - before_blocks;
1097 {
1098 let st = self.stats_mut(typ);
1099 st.index_blocks = index_blocks;
1100 st.index_offset = index_start;
1101 }
1102 let _ = max_level;
1103 Ok(())
1104 }
1105
1106 fn count_index_blocks_marker(&self) -> usize {
1107 self.idx_blocks_total
1108 }
1109
1110 fn flush_index_block(&mut self) -> Result<()> {
1114 let Some(mut bw) = self.block.take() else {
1115 return Ok(());
1116 };
1117 if bw.entries == 0 {
1118 self.block = Some(bw);
1119 return Ok(());
1120 }
1121 let raw_bytes = bw.finish()?;
1122 let mut padding = 0;
1123 if !self.opts.unpadded {
1124 padding = (self.opts.block_size as usize).saturating_sub(raw_bytes);
1125 }
1126 if self.next == 0 {
1127 let hs = self.header_size();
1128 self.write_header_into_block(&mut bw, hs);
1129 }
1130 let data = bw.buf.clone();
1131 self.padded_write(&data, padding);
1132 self.index.push((bw.last_key.clone(), self.next));
1133 self.next += (padding + raw_bytes) as u64;
1134 self.idx_blocks_total += 1;
1135 self.block = None;
1136 Ok(())
1137 }
1138
1139 fn dump_object_index(&mut self) -> Result<()> {
1141 let mut max_common = 1usize;
1143 for w in self.obj_entries.windows(2) {
1144 let n = common_prefix_len(&w[0].hash, &w[1].hash);
1145 if n > max_common {
1146 max_common = n;
1147 }
1148 }
1149 self.object_id_len = max_common + 1;
1150 let id_len = self.object_id_len;
1151
1152 self.reinit_block(BLOCK_TYPE_OBJ);
1153 let entries = std::mem::take(&mut self.obj_entries);
1154 for e in &entries {
1155 let prefix = e.hash[..id_len.min(e.hash.len())].to_vec();
1156 self.add_obj_record(prefix, &e.offsets)?;
1157 }
1158 self.obj_entries = entries;
1159 self.finish_section()
1160 }
1161
1162 fn add_obj_record(&mut self, prefix: Vec<u8>, offsets: &[u64]) -> Result<()> {
1163 let typ = BLOCK_TYPE_OBJ;
1165 if self.block.is_none() {
1166 self.reinit_block(typ);
1167 }
1168 let rec = EncRecord::Obj {
1169 prefix: prefix.clone(),
1170 offsets: offsets.to_vec(),
1171 };
1172 if self.add_to_block(&rec)? {
1173 return Ok(());
1174 }
1175 self.flush_block()?;
1176 self.reinit_block(typ);
1177 if self.add_to_block(&rec)? {
1178 return Ok(());
1179 }
1180 let rec = EncRecord::Obj {
1182 prefix,
1183 offsets: Vec::new(),
1184 };
1185 self.add_to_block(&rec)?;
1186 Ok(())
1187 }
1188
1189 fn finish_public_section(&mut self) -> Result<()> {
1191 let Some(bw) = &self.block else {
1192 return Ok(());
1193 };
1194 let typ = bw.typ;
1195 self.finish_section()?;
1196 if typ == BLOCK_TYPE_REF && !self.opts.skip_index_objects && self.ref_stats.index_blocks > 0
1197 {
1198 self.dump_object_index()?;
1199 }
1200 self.obj_entries.clear();
1201 self.block = None;
1202 self.block_type = 0;
1203 Ok(())
1204 }
1205
1206 fn close(mut self) -> Result<Vec<u8>> {
1208 self.finish_public_section()?;
1209 let empty_table = self.next == 0;
1210 self.pending_padding = 0;
1211
1212 if empty_table {
1213 let hs = self.header_size();
1214 let mut header = vec![0u8; hs];
1215 self.write_header(&mut header);
1216 self.padded_write(&header, 0);
1217 }
1218
1219 let mut footer = vec![0u8; self.header_size()];
1220 self.write_header(&mut footer);
1221 footer.extend_from_slice(&self.ref_stats.index_offset.to_be_bytes());
1222 let obj_field = (self.obj_stats.offset << 5) | (self.object_id_len as u64);
1223 footer.extend_from_slice(&obj_field.to_be_bytes());
1224 footer.extend_from_slice(&self.obj_stats.index_offset.to_be_bytes());
1225 footer.extend_from_slice(&self.log_stats.offset.to_be_bytes());
1226 footer.extend_from_slice(&self.log_stats.index_offset.to_be_bytes());
1227 let crc = crc32(&footer);
1228 footer.extend_from_slice(&crc.to_be_bytes());
1229
1230 self.pending_padding = 0;
1232 self.out.extend_from_slice(&footer);
1233
1234 Ok(self.out)
1235 }
1236}
1237
1238pub struct ReftableReader {
1244 data: Arc<[u8]>,
1245 version: u8,
1246 block_size: u32,
1247 min_update_index: u64,
1248 max_update_index: u64,
1249 ref_index_position: u64,
1250 log_position: u64,
1251}
1252
1253#[derive(Debug)]
1255struct Footer {
1256 block_size: u32,
1257 min_update_index: u64,
1258 max_update_index: u64,
1259 ref_index_position: u64,
1260 log_position: u64,
1261}
1262
1263impl ReftableReader {
1264 pub fn new(data: Vec<u8>) -> Result<Self> {
1266 Self::from_arc(Arc::from(data))
1267 }
1268
1269 pub fn from_arc(data: Arc<[u8]>) -> Result<Self> {
1271 if data.len() < HEADER_SIZE + FOOTER_V1_SIZE {
1272 if data.len() < HEADER_SIZE {
1274 return Err(ReftableError::TooSmall { len: data.len() }.into());
1275 }
1276 }
1277
1278 if &data[0..4] != REFTABLE_MAGIC {
1280 return Err(ReftableError::BadMagic.into());
1281 }
1282 let version = data[4];
1283 if version != 1 && version != 2 {
1284 return Err(ReftableError::UnsupportedVersion { version }.into());
1285 }
1286 let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
1288 if data.len() < footer_size {
1289 return Err(ReftableError::TooSmall { len: data.len() }.into());
1290 }
1291 let footer_start = data.len() - footer_size;
1292 let footer = parse_footer(&data[footer_start..], version)?;
1293
1294 Ok(Self {
1295 data,
1296 version,
1297 block_size: footer.block_size,
1298 min_update_index: footer.min_update_index,
1299 max_update_index: footer.max_update_index,
1300 ref_index_position: footer.ref_index_position,
1301 log_position: footer.log_position,
1302 })
1303 }
1304
1305 fn hash_size(&self) -> usize {
1308 if self.version == 2 {
1309 32
1310 } else {
1311 20
1312 }
1313 }
1314
1315 fn header_len(&self) -> usize {
1317 if self.version == 2 {
1318 28
1319 } else {
1320 HEADER_SIZE
1321 }
1322 }
1323
1324 pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
1326 let mut refs = Vec::new();
1327 self.for_each_ref(|rec| {
1328 refs.push(rec.clone());
1329 ControlFlow::<()>::Continue(())
1330 })?;
1331 Ok(refs)
1332 }
1333
1334 pub fn for_each_ref(&self, f: impl FnMut(&RefRecord) -> ControlFlow<()>) -> Result<()> {
1338 self.for_each_ref_in_range("", None, f)
1339 }
1340
1341 pub fn for_each_ref_in_range(
1343 &self,
1344 lower: &str,
1345 upper_exclusive: Option<&str>,
1346 mut f: impl FnMut(&RefRecord) -> ControlFlow<()>,
1347 ) -> Result<()> {
1348 let footer_size = if self.version == 2 {
1349 72
1350 } else {
1351 FOOTER_V1_SIZE
1352 };
1353 let file_end = self.data.len() - footer_size;
1354
1355 let ref_end = if self.ref_index_position > 0 {
1356 self.ref_index_position as usize
1357 } else if self.log_position > 0 {
1358 self.log_position as usize
1359 } else {
1360 file_end
1361 };
1362
1363 let header_len = self.header_len();
1364 let mut pos = if self.ref_index_position > 0 && !lower.is_empty() {
1365 self.locate_ref_block_via_index(lower.as_bytes())?
1366 .unwrap_or(header_len)
1367 } else {
1368 header_len
1369 };
1370
1371 while pos < ref_end {
1372 if pos >= self.data.len() {
1373 break;
1374 }
1375 let block_type = self.data[pos];
1376 if block_type == 0 {
1377 if self.block_size > 0 {
1378 let bs = self.block_size as usize;
1379 pos = ((pos / bs) + 1) * bs;
1380 continue;
1381 }
1382 break;
1383 }
1384 if block_type != BLOCK_TYPE_REF {
1385 break;
1386 }
1387
1388 let block_len = read_u24(&self.data, pos + 1);
1389 let block_data_start = pos + 4;
1390 let is_first = pos == header_len;
1391 let records_end = if is_first { block_len } else { pos + block_len };
1392
1393 if records_end > ref_end {
1394 break;
1395 }
1396
1397 let restart_table_start =
1398 restart_table_start_for_block(&self.data, records_end, block_data_start)?;
1399
1400 let mut rpos = block_data_start;
1401 let mut prev_name = Vec::<u8>::new();
1402
1403 while rpos < restart_table_start {
1404 let (rec, new_pos) = decode_ref_record(
1405 &self.data,
1406 rpos,
1407 &prev_name,
1408 self.min_update_index,
1409 self.hash_size(),
1410 )?;
1411 prev_name = rec.name.as_bytes().to_vec();
1412 if rec.name.as_str() < lower {
1413 rpos = new_pos;
1414 continue;
1415 }
1416 if let Some(upper) = upper_exclusive {
1417 if rec.name.as_str() >= upper {
1418 return Ok(());
1419 }
1420 }
1421 if f(&rec).is_break() {
1422 return Ok(());
1423 }
1424 rpos = new_pos;
1425 }
1426
1427 if self.block_size > 0 {
1428 let bs = self.block_size as usize;
1429 pos = if is_first { bs } else { pos + bs };
1430 } else {
1431 pos = records_end;
1432 }
1433 }
1434
1435 Ok(())
1436 }
1437
1438 pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
1440 if self.ref_index_position > 0 {
1441 if let Some(rec) = self.lookup_ref_via_index(name)? {
1442 return Ok(Some(rec));
1443 }
1444 }
1445 let refs = self.read_refs()?;
1446 Ok(refs.into_iter().find(|r| r.name == name))
1447 }
1448
1449 fn lookup_ref_via_index(&self, name: &str) -> Result<Option<RefRecord>> {
1451 let Some(block_off) = self.locate_ref_block_via_index(name.as_bytes())? else {
1452 return Ok(None);
1453 };
1454 self.find_ref_in_block(block_off, name)
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 find_ref_in_block(&self, block_pos: usize, name: &str) -> Result<Option<RefRecord>> {
1504 let (records_start, records_end) = self.block_record_range(block_pos)?;
1505 let mut rpos = records_start;
1506 let mut prev_name = Vec::<u8>::new();
1507 while rpos < records_end {
1508 let (rec, new_pos) = decode_ref_record(
1509 &self.data,
1510 rpos,
1511 &prev_name,
1512 self.min_update_index,
1513 self.hash_size(),
1514 )?;
1515 prev_name = rec.name.as_bytes().to_vec();
1516 match rec.name.as_str().cmp(name) {
1517 std::cmp::Ordering::Equal => return Ok(Some(rec)),
1518 std::cmp::Ordering::Greater => return Ok(None),
1519 std::cmp::Ordering::Less => {}
1520 }
1521 rpos = new_pos;
1522 }
1523 Ok(None)
1524 }
1525
1526 fn block_record_range(&self, block_pos: usize) -> Result<(usize, usize)> {
1527 if block_pos + 4 > self.data.len() {
1528 return Err(truncated(ReftableField::BlockHeader));
1529 }
1530 let block_len = read_u24(&self.data, block_pos + 1);
1531 let header_len = self.header_len();
1532 let is_first = block_pos == header_len;
1533 let records_end = if is_first {
1534 block_len
1535 } else {
1536 block_pos + block_len
1537 };
1538 if records_end > self.data.len() || records_end < 2 {
1539 return Err(corrupt(ReftableError::InvalidBlockLength));
1540 }
1541 let records_data_start = block_pos + 4;
1542 let restart_table_start =
1543 restart_table_start_for_block(&self.data, records_end, records_data_start)?;
1544 Ok((records_data_start, restart_table_start))
1545 }
1546
1547 pub fn read_logs(&self) -> Result<Vec<LogRecord>> {
1549 let footer_size = if self.version == 2 {
1550 72
1551 } else {
1552 FOOTER_V1_SIZE
1553 };
1554 let file_end = self.data.len() - footer_size;
1555
1556 let mut pos = if self.log_position > 0 {
1564 self.log_position as usize
1565 } else if self.data.len() > self.header_len()
1566 && self.data[self.header_len()] == BLOCK_TYPE_LOG
1567 {
1568 self.header_len()
1570 } else {
1571 return Ok(Vec::new());
1572 };
1573 let mut logs = Vec::new();
1574
1575 while pos < file_end {
1576 if pos >= self.data.len() {
1577 break;
1578 }
1579 let block_type = self.data[pos];
1580 if block_type != BLOCK_TYPE_LOG {
1581 break;
1582 }
1583 let is_first = pos == self.header_len() && self.log_position == 0;
1588 let block_len = read_u24(&self.data, pos + 1);
1589 let compressed_start = pos + 4;
1590
1591 let header_prefix = if is_first { self.header_len() } else { 0 };
1594 let inflated_size = block_len.saturating_sub(4 + header_prefix);
1595
1596 let remaining = &self.data[compressed_start..file_end];
1597 let (inflated, consumed) = decompress_log_payload(remaining, inflated_size)?;
1598
1599 if inflated.len() < 2 {
1602 break;
1603 }
1604 let records_end = inflated.len();
1605 let restart_table_start = restart_table_start_for_block(&inflated, records_end, 0)?;
1606
1607 let mut rpos = 0usize;
1608 let mut prev_key = Vec::<u8>::new();
1609
1610 while rpos < restart_table_start {
1611 let (log, new_pos) =
1612 decode_log_record(&inflated, rpos, &prev_key, self.hash_size())?;
1613 let mut key = Vec::new();
1615 key.extend_from_slice(log.refname.as_bytes());
1616 key.push(0);
1617 key.extend_from_slice(&(0xffffffffffffffffu64 - log.update_index).to_be_bytes());
1618 prev_key = key;
1619 logs.push(log);
1620 rpos = new_pos;
1621 }
1622
1623 pos = compressed_start + consumed;
1624 }
1625
1626 Ok(logs)
1627 }
1628
1629 pub fn block_size(&self) -> u32 {
1631 self.block_size
1632 }
1633
1634 pub fn min_update_index(&self) -> u64 {
1636 self.min_update_index
1637 }
1638
1639 pub fn max_update_index(&self) -> u64 {
1641 self.max_update_index
1642 }
1643
1644 pub fn ref_index_offset(&self) -> u64 {
1646 self.ref_index_position
1647 }
1648
1649 pub fn payload_size(&self) -> u64 {
1651 let overhead = if self.version == 2 { 28 } else { 24 };
1652 self.data.len().saturating_sub(overhead) as u64
1653 }
1654}
1655
1656fn decode_index_record(data: &[u8], pos: usize, prev_key: &[u8]) -> Result<(Vec<u8>, u64, usize)> {
1661 let (prefix_len, p) = get_varint(data, pos)?;
1662 let (suffix_and_type, mut p) = get_varint(data, p)?;
1663 let suffix_len = (suffix_and_type >> 3) as usize;
1664
1665 let mut key = Vec::with_capacity(prefix_len as usize + suffix_len);
1666 if prefix_len > 0 {
1667 if (prefix_len as usize) > prev_key.len() {
1668 return Err(corrupt(ReftableError::PrefixTooLong {
1669 field: ReftableField::IndexKey,
1670 }));
1671 }
1672 key.extend_from_slice(&prev_key[..prefix_len as usize]);
1673 }
1674 if p + suffix_len > data.len() {
1675 return Err(truncated(ReftableField::IndexKey));
1676 }
1677 key.extend_from_slice(&data[p..p + suffix_len]);
1678 p += suffix_len;
1679
1680 let (offset, p) = get_varint(data, p)?;
1681 Ok((key, offset, p))
1682}
1683
1684fn decode_ref_record(
1685 data: &[u8],
1686 pos: usize,
1687 prev_name: &[u8],
1688 min_update_index: u64,
1689 hash_size: usize,
1690) -> Result<(RefRecord, usize)> {
1691 let (prefix_len, p) = get_varint(data, pos)?;
1692 let (suffix_and_type, mut p) = get_varint(data, p)?;
1693 let suffix_len = (suffix_and_type >> 3) as usize;
1694 let value_type = (suffix_and_type & 0x7) as u8;
1695
1696 let mut name = Vec::with_capacity(prefix_len as usize + suffix_len);
1698 if prefix_len > 0 {
1699 if (prefix_len as usize) > prev_name.len() {
1700 return Err(corrupt(ReftableError::PrefixTooLong {
1701 field: ReftableField::RefName,
1702 }));
1703 }
1704 name.extend_from_slice(&prev_name[..prefix_len as usize]);
1705 }
1706 if p + suffix_len > data.len() {
1707 return Err(truncated(ReftableField::RefName));
1708 }
1709 name.extend_from_slice(&data[p..p + suffix_len]);
1710 p += suffix_len;
1711
1712 let name_str = String::from_utf8(name).map_err(|_| invalid_utf8(ReftableField::RefName))?;
1713
1714 let (update_index_delta, mut p) = get_varint(data, p)?;
1715 let update_index = min_update_index + update_index_delta;
1716
1717 let value = match value_type {
1718 VALUE_DELETION => RefValue::Deletion,
1719 VALUE_ONE_OID => {
1720 if p + hash_size > data.len() {
1721 return Err(truncated(ReftableField::ObjectId));
1722 }
1723 let oid = ObjectId::from_bytes(&data[p..p + hash_size])?;
1724 p += hash_size;
1725 RefValue::Val1(oid)
1726 }
1727 VALUE_TWO_OID => {
1728 if p + 2 * hash_size > data.len() {
1729 return Err(truncated(ReftableField::ObjectIdPair));
1730 }
1731 let oid = ObjectId::from_bytes(&data[p..p + hash_size])?;
1732 p += hash_size;
1733 let peeled = ObjectId::from_bytes(&data[p..p + hash_size])?;
1734 p += hash_size;
1735 RefValue::Val2(oid, peeled)
1736 }
1737 VALUE_SYMREF => {
1738 let (target_len, p2) = get_varint(data, p)?;
1739 p = p2;
1740 let target_len = target_len as usize;
1741 if p + target_len > data.len() {
1742 return Err(truncated(ReftableField::SymrefTarget));
1743 }
1744 let target = String::from_utf8(data[p..p + target_len].to_vec())
1745 .map_err(|_| invalid_utf8(ReftableField::SymrefTarget))?;
1746 p += target_len;
1747 RefValue::Symref(target)
1748 }
1749 _ => {
1750 return Err(corrupt(ReftableError::UnknownValueType { value_type }));
1751 }
1752 };
1753
1754 Ok((
1755 RefRecord {
1756 name: name_str,
1757 update_index,
1758 value,
1759 },
1760 p,
1761 ))
1762}
1763
1764fn decode_log_record(
1765 data: &[u8],
1766 pos: usize,
1767 prev_key: &[u8],
1768 hash_size: usize,
1769) -> Result<(LogRecord, usize)> {
1770 let (prefix_len, p) = get_varint(data, pos)?;
1771 let (suffix_and_type, mut p) = get_varint(data, p)?;
1772 let suffix_len = (suffix_and_type >> 3) as usize;
1773 let log_type = (suffix_and_type & 0x7) as u8;
1774
1775 let mut key = Vec::with_capacity(prefix_len as usize + suffix_len);
1777 if prefix_len > 0 {
1778 if (prefix_len as usize) > prev_key.len() {
1779 return Err(corrupt(ReftableError::PrefixTooLong {
1780 field: ReftableField::LogKey,
1781 }));
1782 }
1783 key.extend_from_slice(&prev_key[..prefix_len as usize]);
1784 }
1785 if p + suffix_len > data.len() {
1786 return Err(truncated(ReftableField::LogKey));
1787 }
1788 key.extend_from_slice(&data[p..p + suffix_len]);
1789 p += suffix_len;
1790
1791 let null_pos = key
1793 .iter()
1794 .position(|&b| b == 0)
1795 .ok_or_else(|| corrupt(ReftableError::InvalidLogKey))?;
1796 let refname = String::from_utf8(key[..null_pos].to_vec())
1797 .map_err(|_| invalid_utf8(ReftableField::RefName))?;
1798 if null_pos + 9 > key.len() {
1799 return Err(corrupt(ReftableError::InvalidLogKey));
1800 }
1801 let reversed_idx = u64::from_be_bytes(
1802 key[null_pos + 1..null_pos + 9]
1803 .try_into()
1804 .map_err(|_| corrupt(ReftableError::InvalidLogKey))?,
1805 );
1806 let update_index = 0xffffffffffffffffu64 - reversed_idx;
1807
1808 if log_type == 0 {
1809 let zero_oid = ObjectId::from_bytes(&vec![0u8; hash_size])?;
1811 return Ok((
1812 LogRecord {
1813 refname,
1814 update_index,
1815 old_id: zero_oid,
1816 new_id: zero_oid,
1817 name: String::new(),
1818 email: String::new(),
1819 time_seconds: 0,
1820 tz_offset: 0,
1821 message: String::new(),
1822 },
1823 p,
1824 ));
1825 }
1826
1827 if p + 2 * hash_size > data.len() {
1829 return Err(truncated(ReftableField::ObjectIdPair));
1830 }
1831 let old_id = ObjectId::from_bytes(&data[p..p + hash_size])?;
1832 p += hash_size;
1833 let new_id = ObjectId::from_bytes(&data[p..p + hash_size])?;
1834 p += hash_size;
1835
1836 let (name_len, p2) = get_varint(data, p)?;
1837 p = p2;
1838 let name_len = name_len as usize;
1839 if p + name_len > data.len() {
1840 return Err(truncated(ReftableField::LogName));
1841 }
1842 let name = String::from_utf8(data[p..p + name_len].to_vec())
1843 .map_err(|_| invalid_utf8(ReftableField::LogName))?;
1844 p += name_len;
1845
1846 let (email_len, p2) = get_varint(data, p)?;
1847 p = p2;
1848 let email_len = email_len as usize;
1849 if p + email_len > data.len() {
1850 return Err(truncated(ReftableField::LogEmail));
1851 }
1852 let email = String::from_utf8(data[p..p + email_len].to_vec())
1853 .map_err(|_| invalid_utf8(ReftableField::LogEmail))?;
1854 p += email_len;
1855
1856 let (time_seconds, p2) = get_varint(data, p)?;
1857 p = p2;
1858
1859 if p + 2 > data.len() {
1860 return Err(truncated(ReftableField::LogTimezone));
1861 }
1862 let tz_offset = i16::from_be_bytes([data[p], data[p + 1]]);
1863 p += 2;
1864
1865 let (msg_len, p2) = get_varint(data, p)?;
1866 p = p2;
1867 let msg_len = msg_len as usize;
1868 if p + msg_len > data.len() {
1869 return Err(truncated(ReftableField::LogMessage));
1870 }
1871 let message = String::from_utf8(data[p..p + msg_len].to_vec())
1872 .map_err(|_| invalid_utf8(ReftableField::LogMessage))?;
1873 p += msg_len;
1874
1875 Ok((
1876 LogRecord {
1877 refname,
1878 update_index,
1879 old_id,
1880 new_id,
1881 name,
1882 email,
1883 time_seconds,
1884 tz_offset,
1885 message,
1886 },
1887 p,
1888 ))
1889}
1890
1891pub(crate) fn widen_oid_to(oid: ObjectId, hash_size: usize) -> ObjectId {
1899 if oid.is_zero() && oid.as_bytes().len() != hash_size {
1900 ObjectId::from_bytes(&vec![0u8; hash_size]).unwrap_or(oid)
1901 } else {
1902 oid
1903 }
1904}
1905
1906fn reftable_hash_size_for_git_dir(git_dir: &Path) -> usize {
1909 let cfg = crate::config::ConfigSet::load(
1910 &crate::environment::Environment::empty(),
1911 Some(git_dir),
1912 true,
1913 )
1914 .unwrap_or_default();
1915 match cfg
1916 .get("extensions.objectformat")
1917 .and_then(|v| crate::objects::HashAlgo::from_name(&v))
1918 {
1919 Some(crate::objects::HashAlgo::Sha256) => 32,
1920 _ => 20,
1921 }
1922}
1923
1924fn prefix_upper_exclusive(prefix: &str) -> Option<String> {
1927 if prefix.is_empty() || prefix.ends_with('/') {
1928 return None;
1929 }
1930 let mut end = prefix.as_bytes().to_vec();
1931 while let Some(last) = end.last_mut() {
1932 if *last == b'/' {
1933 break;
1934 }
1935 if *last < 0xff {
1936 *last += 1;
1937 return String::from_utf8(end).ok();
1938 }
1939 *last = 0;
1940 end.pop();
1941 }
1942 None
1943}
1944
1945fn suggest_compaction_segment(sizes: &[u64]) -> (usize, usize) {
1947 let n = sizes.len();
1948 if n <= 1 {
1949 return (0, 0);
1950 }
1951 let mut seg_end = 0usize;
1952 let mut bytes = 0u64;
1953 let mut i = n - 1;
1954 while i > 0 {
1955 if sizes[i - 1] < sizes[i].saturating_mul(2) {
1956 seg_end = i + 1;
1957 bytes = sizes[i];
1958 break;
1959 }
1960 i -= 1;
1961 }
1962 if seg_end == 0 {
1963 return (0, 0);
1964 }
1965 let mut seg_start = 0usize;
1966 i = seg_end.saturating_sub(1);
1967 while i > 0 {
1968 let curr = bytes;
1969 bytes = bytes.saturating_add(sizes[i - 1]);
1970 if sizes[i - 1] < curr.saturating_mul(2) {
1971 seg_start = i - 1;
1972 }
1973 i -= 1;
1974 }
1975 if seg_end <= seg_start + 1 {
1976 return (0, 0);
1977 }
1978 (seg_start, seg_end)
1979}
1980
1981#[derive(Clone, PartialEq, Eq, Hash)]
1982struct StackCacheKey {
1983 reftable_dir: PathBuf,
1984 tables_list_len: u64,
1985 tables_list_modified: i64,
1986}
1987
1988struct CachedStackTables {
1989 table_names: Vec<String>,
1990 readers: Vec<ReftableReader>,
1991}
1992
1993static STACK_TABLE_CACHE: OnceLock<Mutex<HashMap<StackCacheKey, Arc<CachedStackTables>>>> =
1995 OnceLock::new();
1996
1997fn stack_table_cache() -> &'static Mutex<HashMap<StackCacheKey, Arc<CachedStackTables>>> {
1998 STACK_TABLE_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
1999}
2000
2001fn invalidate_stack_table_cache(reftable_dir: &Path) {
2002 let Ok(mut guard) = stack_table_cache().lock() else {
2003 return;
2004 };
2005 guard.retain(|k, _| k.reftable_dir != reftable_dir);
2006}
2007
2008fn tables_list_cache_key(reftable_dir: &Path) -> Result<StackCacheKey> {
2009 let tables_list = reftable_dir.join("tables.list");
2010 match fs::metadata(&tables_list) {
2011 Ok(meta) => Ok(StackCacheKey {
2012 reftable_dir: reftable_dir.to_path_buf(),
2013 tables_list_len: meta.len(),
2014 tables_list_modified: meta
2015 .modified()
2016 .ok()
2017 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
2018 .map(|d| d.as_secs() as i64)
2019 .unwrap_or(0),
2020 }),
2021 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(StackCacheKey {
2022 reftable_dir: reftable_dir.to_path_buf(),
2023 tables_list_len: 0,
2024 tables_list_modified: 0,
2025 }),
2026 Err(e) => Err(Error::Io(e)),
2027 }
2028}
2029
2030fn build_cached_stack_tables(
2031 reftable_dir: &Path,
2032 table_names: &[String],
2033) -> Result<CachedStackTables> {
2034 let mut readers = Vec::with_capacity(table_names.len());
2035 for name in table_names {
2036 let path = reftable_dir.join(name);
2037 let data = fs::read(&path).map_err(Error::Io)?;
2038 readers.push(ReftableReader::from_arc(Arc::from(data))?);
2039 }
2040 Ok(CachedStackTables {
2041 table_names: table_names.to_owned(),
2042 readers,
2043 })
2044}
2045
2046fn load_cached_stack_tables(reftable_dir: &Path) -> Result<Arc<CachedStackTables>> {
2047 let key = tables_list_cache_key(reftable_dir)?;
2048 if let Ok(guard) = stack_table_cache().lock() {
2049 if let Some(hit) = guard.get(&key) {
2050 return Ok(Arc::clone(hit));
2051 }
2052 }
2053
2054 let tables_list = reftable_dir.join("tables.list");
2055 let table_names: Vec<String> = match fs::read_to_string(&tables_list) {
2056 Ok(content) => content
2057 .lines()
2058 .filter(|l| !l.is_empty())
2059 .map(|l| l.to_owned())
2060 .collect(),
2061 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
2062 Err(e) => return Err(Error::Io(e)),
2063 };
2064 let cached = Arc::new(build_cached_stack_tables(reftable_dir, &table_names)?);
2065 if let Ok(mut guard) = stack_table_cache().lock() {
2066 guard.insert(key, Arc::clone(&cached));
2067 }
2068 Ok(cached)
2069}
2070
2071fn cached_stack_tables_for_names(
2077 reftable_dir: &Path,
2078 table_names: &[String],
2079) -> Result<Arc<CachedStackTables>> {
2080 let cached = load_cached_stack_tables(reftable_dir)?;
2081 if cached.table_names.as_slice() == table_names {
2082 return Ok(cached);
2083 }
2084 invalidate_stack_table_cache(reftable_dir);
2085 let cached = load_cached_stack_tables(reftable_dir)?;
2086 if cached.table_names.as_slice() == table_names {
2087 return Ok(cached);
2088 }
2089 let key = tables_list_cache_key(reftable_dir)?;
2090 let rebuilt = Arc::new(build_cached_stack_tables(reftable_dir, table_names)?);
2091 if let Ok(mut guard) = stack_table_cache().lock() {
2092 guard.insert(key, Arc::clone(&rebuilt));
2093 }
2094 Ok(rebuilt)
2095}
2096
2097fn cached_reader_for_name<'a>(
2098 cached: &'a CachedStackTables,
2099 table_name: &str,
2100) -> Result<&'a ReftableReader> {
2101 let idx = cached
2102 .table_names
2103 .iter()
2104 .position(|n| n == table_name)
2105 .ok_or_else(|| ReftableError::MissingTable {
2106 name: table_name.to_owned(),
2107 })?;
2108 Ok(&cached.readers[idx])
2109}
2110
2111pub struct ReftableStack {
2116 reftable_dir: PathBuf,
2118 table_names: Vec<String>,
2120 fixed_table_names: bool,
2122}
2123
2124fn parse_tables_list_content(content: &str) -> Vec<String> {
2125 content
2126 .lines()
2127 .filter(|l| !l.is_empty())
2128 .map(|l| l.to_owned())
2129 .collect()
2130}
2131
2132fn read_tables_list_names(reftable_dir: &Path) -> Result<Vec<String>> {
2133 let tables_list = reftable_dir.join("tables.list");
2134 match fs::read_to_string(&tables_list) {
2135 Ok(content) => Ok(parse_tables_list_content(&content)),
2136 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
2137 Err(e) => Err(Error::Io(e)),
2138 }
2139}
2140
2141pub(crate) struct TablesListLock {
2144 lock: Option<crate::lockfile::LockFile>,
2145}
2146
2147impl TablesListLock {
2148 fn new(lock: crate::lockfile::LockFile) -> Self {
2149 Self { lock: Some(lock) }
2150 }
2151
2152 fn path(&self) -> Result<&Path> {
2153 self.lock
2154 .as_ref()
2155 .map(|lock| lock.lock_path())
2156 .ok_or_else(|| ReftableError::LockReleased.into())
2157 }
2158}
2159
2160impl Drop for TablesListLock {
2161 fn drop(&mut self) {
2162 drop(self.lock.take());
2163 }
2164}
2165
2166impl ReftableStack {
2167 fn hash_size(&self) -> usize {
2170 match self.reftable_dir.parent() {
2171 Some(git_dir) => reftable_hash_size_for_git_dir(git_dir),
2172 None => 20,
2173 }
2174 }
2175
2176 pub fn open(git_dir: &Path) -> Result<Self> {
2181 let reftable_dir = git_dir.join("reftable");
2182 fs::create_dir_all(&reftable_dir).map_err(Error::Io)?;
2183 let tables_list = reftable_dir.join("tables.list");
2184 let table_names = match fs::read_to_string(&tables_list) {
2185 Ok(content) => parse_tables_list_content(&content),
2186 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
2187 Err(e) => return Err(Error::Io(e)),
2188 };
2189 Ok(Self {
2190 reftable_dir,
2191 table_names,
2192 fixed_table_names: false,
2193 })
2194 }
2195
2196 fn subset_stack(reftable_dir: PathBuf, table_names: Vec<String>) -> Self {
2197 Self {
2198 reftable_dir,
2199 table_names,
2200 fixed_table_names: true,
2201 }
2202 }
2203
2204 fn table_names_for_read(&self) -> Result<Vec<String>> {
2205 if self.fixed_table_names {
2206 Ok(self.table_names.clone())
2207 } else {
2208 read_tables_list_names(&self.reftable_dir)
2209 }
2210 }
2211
2212 fn first_ref_name_with_prefix(&self, prefix: &str) -> Result<Option<String>> {
2214 let names = self.table_names_for_read()?;
2215 let cached = cached_stack_tables_for_names(&self.reftable_dir, &names)?;
2216 let readers = self.stack_readers(&cached, &names)?;
2217 let upper = prefix_upper_exclusive(prefix.trim_end_matches('/'));
2218 let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
2219 for reader in readers {
2220 reader.for_each_ref_in_range(prefix, upper.as_deref(), |rec| {
2221 if !rec.name.starts_with(prefix) || rec.name.len() <= prefix.len() {
2222 return ControlFlow::<()>::Continue(());
2223 }
2224 match &rec.value {
2225 RefValue::Deletion => {
2226 merged.remove(&rec.name);
2227 }
2228 _ => {
2229 merged.insert(rec.name.clone(), rec.clone());
2230 }
2231 }
2232 ControlFlow::<()>::Continue(())
2233 })?;
2234 }
2235 Ok(merged.keys().next().cloned())
2236 }
2237
2238 fn write_options(&self) -> WriteOptions {
2240 let git_dir = self
2241 .reftable_dir
2242 .parent()
2243 .map(|p| p.to_path_buf())
2244 .unwrap_or_else(|| self.reftable_dir.clone());
2245 read_write_options(&git_dir)
2246 }
2247
2248 fn stack_readers<'a>(
2249 &self,
2250 cached: &'a CachedStackTables,
2251 table_names: &[String],
2252 ) -> Result<Vec<&'a ReftableReader>> {
2253 table_names
2254 .iter()
2255 .map(|name| cached_reader_for_name(cached, name))
2256 .collect()
2257 }
2258
2259 pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
2264 let names = self.table_names_for_read()?;
2265 let cached = cached_stack_tables_for_names(&self.reftable_dir, &names)?;
2266 let readers = self.stack_readers(&cached, &names)?;
2267 let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
2268 for reader in readers {
2269 reader.for_each_ref(|rec| {
2270 match &rec.value {
2271 RefValue::Deletion => {
2272 merged.remove(&rec.name);
2273 }
2274 _ => {
2275 merged.insert(rec.name.clone(), rec.clone());
2276 }
2277 }
2278 ControlFlow::<()>::Continue(())
2279 })?;
2280 }
2281 Ok(merged.into_values().collect())
2282 }
2283
2284 pub fn list_refs_under_prefix(
2287 &self,
2288 git_dir: &Path,
2289 prefix: &str,
2290 ) -> Result<Vec<(String, ObjectId)>> {
2291 let names = self.table_names_for_read()?;
2292 let cached = cached_stack_tables_for_names(&self.reftable_dir, &names)?;
2293 let readers = self.stack_readers(&cached, &names)?;
2294 let lower = prefix;
2295 let upper = prefix_upper_exclusive(prefix);
2296 let mut out = Vec::new();
2297
2298 if readers.len() == 1 {
2299 let reader = readers[0];
2300 reader.for_each_ref_in_range(lower, upper.as_deref(), |rec| {
2301 if prefix.ends_with('/') && !rec.name.starts_with(prefix) {
2302 return ControlFlow::<()>::Continue(());
2303 }
2304 push_list_ref_entry(git_dir, rec, &mut out);
2305 ControlFlow::<()>::Continue(())
2306 })?;
2307 out.sort_by(|a, b| a.0.cmp(&b.0));
2308 return Ok(out);
2309 }
2310
2311 let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
2312 for reader in readers {
2313 reader.for_each_ref_in_range(lower, upper.as_deref(), |rec| {
2314 if prefix.ends_with('/') && !rec.name.starts_with(prefix) {
2315 return ControlFlow::<()>::Continue(());
2316 }
2317 match &rec.value {
2318 RefValue::Deletion => {
2319 merged.remove(&rec.name);
2320 }
2321 _ => {
2322 merged.insert(rec.name.clone(), rec.clone());
2323 }
2324 }
2325 ControlFlow::<()>::Continue(())
2326 })?;
2327 }
2328 for rec in merged.into_values() {
2329 push_list_ref_entry(git_dir, &rec, &mut out);
2330 }
2331 out.sort_by(|a, b| a.0.cmp(&b.0));
2332 Ok(out)
2333 }
2334
2335 fn table_payload_sizes(&self) -> Result<Vec<u64>> {
2336 let names = self.table_names_for_read()?;
2337 let cached = cached_stack_tables_for_names(&self.reftable_dir, &names)?;
2338 Ok(self
2339 .stack_readers(&cached, &names)?
2340 .into_iter()
2341 .map(ReftableReader::payload_size)
2342 .collect())
2343 }
2344
2345 fn auto_compact_geometric(&mut self, opts: &WriteOptions) -> Result<()> {
2346 if !opts.autocompaction_enabled {
2347 return Ok(());
2348 }
2349 self.reload_table_names();
2350 if self.table_names.len() <= 2 {
2352 return Ok(());
2353 }
2354 let sizes = self.table_payload_sizes()?;
2355 let (start, end) = suggest_compaction_segment(&sizes);
2356 if end <= start + 1 {
2357 return Ok(());
2358 }
2359 self.compact_range(start, end - 1, opts)
2360 }
2361
2362 fn compact_range(&mut self, first: usize, last: usize, opts: &WriteOptions) -> Result<()> {
2363 let mut guard = self.acquire_tables_list_lock()?;
2364 self.reload_table_names();
2365 if first >= self.table_names.len() || last >= self.table_names.len() || first > last {
2366 return Ok(());
2367 }
2368 let compact_names: Vec<String> = self.table_names[first..=last].to_vec();
2369 let prefix_stack = Self::subset_stack(self.reftable_dir.clone(), compact_names.clone());
2370 let refs = prefix_stack.read_refs()?;
2371 let logs = prefix_stack.read_all_logs()?;
2372
2373 let mut min_idx = u64::MAX;
2374 let mut max_idx = 0u64;
2375 for name in &compact_names {
2376 let path = self.reftable_dir.join(name);
2377 let data = fs::read(&path).map_err(Error::Io)?;
2378 let reader = ReftableReader::new(data)?;
2379 min_idx = min_idx.min(reader.min_update_index());
2380 max_idx = max_idx.max(reader.max_update_index());
2381 }
2382 if min_idx == u64::MAX {
2383 min_idx = 0;
2384 }
2385
2386 let wopts = WriteOptions {
2387 hash_size: self.hash_size(),
2388 ..opts.clone()
2389 };
2390 let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
2391 for rec in refs {
2392 writer.add_ref(rec)?;
2393 }
2394 for log in logs {
2395 writer.add_log(log)?;
2396 }
2397 let data = writer.finish()?;
2398 let filename = self.write_table_file(&data, max_idx)?;
2399
2400 let mut new_names: Vec<String> = self.table_names[..first].to_vec();
2401 new_names.push(filename.clone());
2402 new_names.extend_from_slice(&self.table_names[last + 1..]);
2403 self.table_names = new_names;
2404 self.write_tables_list_locked(&mut guard)?;
2405 invalidate_stack_table_cache(&self.reftable_dir);
2406 for old in &compact_names {
2407 if old == &filename {
2408 continue;
2409 }
2410 let _ = fs::remove_file(self.reftable_dir.join(old));
2411 }
2412 Ok(())
2413 }
2414
2415 pub fn lookup_ref_oid(&self, name: &str) -> Result<Option<ObjectId>> {
2417 Ok(self.lookup_ref(name)?.and_then(|rec| match rec.value {
2418 RefValue::Val1(oid) => Some(oid),
2419 RefValue::Val2(oid, _) => Some(oid),
2420 RefValue::Deletion | RefValue::Symref(_) => None,
2421 }))
2422 }
2423
2424 pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
2426 let names = self.table_names_for_read()?;
2427 let cached = cached_stack_tables_for_names(&self.reftable_dir, &names)?;
2428 for reader in self.stack_readers(&cached, &names)?.into_iter().rev() {
2429 if let Some(rec) = reader.lookup_ref(name)? {
2430 return match rec.value {
2431 RefValue::Deletion => Ok(None),
2432 _ => Ok(Some(rec)),
2433 };
2434 }
2435 }
2436 Ok(None)
2437 }
2438
2439 pub fn read_logs_for_ref(&self, refname: &str) -> Result<Vec<LogRecord>> {
2441 let mut logs = Vec::new();
2442 for table_name in &self.table_names {
2443 let path = self.reftable_dir.join(table_name);
2444 let data = fs::read(&path).map_err(Error::Io)?;
2445 let reader = ReftableReader::new(data)?;
2446 for log in reader.read_logs()? {
2447 if log.refname == refname {
2448 logs.push(log);
2449 }
2450 }
2451 }
2452 logs.sort_by_key(|a| std::cmp::Reverse(a.update_index));
2454 Ok(logs)
2455 }
2456
2457 pub fn replace_logs_for_ref(
2459 &mut self,
2460 refname: &str,
2461 entries: &[crate::reflog::ReflogEntry],
2462 ) -> Result<()> {
2463 let refs = self.read_refs()?;
2464 let mut logs: Vec<LogRecord> = self
2465 .read_all_logs()?
2466 .into_iter()
2467 .filter(|log| log.refname != refname)
2468 .collect();
2469 let mut next_update_index = self.max_update_index()? + 1;
2470 let hash_size = self.hash_size();
2471 for entry in entries {
2472 let (name, email, time_secs, tz) = parse_identity_string(&entry.identity);
2473 logs.push(LogRecord {
2474 refname: refname.to_owned(),
2475 update_index: next_update_index,
2476 old_id: widen_oid_to(entry.old_oid, hash_size),
2477 new_id: widen_oid_to(entry.new_oid, hash_size),
2478 name,
2479 email,
2480 time_seconds: time_secs,
2481 tz_offset: tz,
2482 message: entry.message.clone(),
2483 });
2484 next_update_index += 1;
2485 }
2486
2487 let mut min_idx = u64::MAX;
2488 let mut max_idx = 0u64;
2489 for name in &self.table_names {
2490 let path = self.reftable_dir.join(name);
2491 let data = fs::read(&path).map_err(Error::Io)?;
2492 let reader = ReftableReader::new(data)?;
2493 min_idx = min_idx.min(reader.min_update_index());
2494 max_idx = max_idx.max(reader.max_update_index());
2495 }
2496 if min_idx == u64::MAX {
2497 min_idx = 0;
2498 }
2499 max_idx = max_idx.max(next_update_index.saturating_sub(1));
2500
2501 let wopts = WriteOptions {
2502 hash_size: self.hash_size(),
2503 ..Default::default()
2504 };
2505 let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
2506 for rec in refs {
2507 writer.add_ref(rec)?;
2508 }
2509 for log in logs {
2510 writer.add_log(log)?;
2511 }
2512 let data = writer.finish()?;
2513 let old_names = self.table_names.clone();
2514 let name = self.write_table_file(&data, max_idx)?;
2515 self.table_names = vec![name];
2516 self.write_tables_list()?;
2517 for old in &old_names {
2518 let _ = fs::remove_file(self.reftable_dir.join(old));
2519 }
2520 Ok(())
2521 }
2522
2523 pub fn read_all_logs(&self) -> Result<Vec<LogRecord>> {
2525 let mut logs = Vec::new();
2526 for table_name in &self.table_names {
2527 let path = self.reftable_dir.join(table_name);
2528 let data = fs::read(&path).map_err(Error::Io)?;
2529 let reader = ReftableReader::new(data)?;
2530 logs.extend(reader.read_logs()?);
2531 }
2532 logs.sort_by(|a, b| {
2533 a.refname
2534 .cmp(&b.refname)
2535 .then_with(|| b.update_index.cmp(&a.update_index))
2536 });
2537 Ok(logs)
2538 }
2539
2540 pub fn max_update_index(&self) -> Result<u64> {
2548 let names: Vec<String> = match fs::read_to_string(self.reftable_dir.join("tables.list")) {
2549 Ok(content) => content
2550 .lines()
2551 .filter(|line| !line.is_empty())
2552 .map(ToOwned::to_owned)
2553 .collect(),
2554 Err(_) => self.table_names.clone(),
2555 };
2556 let mut max_idx = 0u64;
2557 for name in &names {
2558 let path = self.reftable_dir.join(name);
2559 let data = match fs::read(&path) {
2560 Ok(data) => data,
2561 Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
2562 Err(err) => return Err(Error::Io(err)),
2563 };
2564 let reader = ReftableReader::new(data)?;
2565 max_idx = max_idx.max(reader.max_update_index());
2566 }
2567 Ok(max_idx)
2568 }
2569
2570 pub fn add_table(
2575 &mut self,
2576 data: &[u8],
2577 update_index: u64,
2578 opts: &WriteOptions,
2579 ) -> Result<String> {
2580 let table_has_deletion = ReftableReader::new(data.to_vec())
2581 .and_then(|reader| reader.read_refs())
2582 .map(|records| {
2583 records
2584 .iter()
2585 .any(|record| matches!(record.value, RefValue::Deletion))
2586 })
2587 .unwrap_or(false);
2588 let random: u64 = {
2589 let mut buf = [0u8; 8];
2591 if let Ok(mut f) = fs::File::open("/dev/urandom") {
2592 let _ = f.read(&mut buf);
2593 }
2594 u64::from_le_bytes(buf)
2595 };
2596 let filename = format!(
2597 "{:08x}-{:08x}-{:08x}.ref",
2598 update_index, update_index, random as u32
2599 );
2600 let path = self.reftable_dir.join(&filename);
2601 fs::write(&path, data).map_err(Error::Io)?;
2602
2603 {
2608 let mut guard = self.acquire_tables_list_lock()?;
2609 self.reload_table_names();
2610 self.table_names.push(filename.clone());
2611 self.write_tables_list_locked(&mut guard)?;
2612 }
2613
2614 if table_has_deletion && self.table_names.len() > 2 {
2618 self.compact_prefix_preserving_newest(opts)?;
2619 } else if opts.autocompaction_enabled {
2620 if self
2621 .table_names
2622 .iter()
2623 .any(|name| self.table_is_locked(name))
2624 {
2625 self.compact_unlocked_suffix()?;
2626 } else {
2627 self.auto_compact_geometric(opts)?;
2628 }
2629 }
2630
2631 Ok(filename)
2632 }
2633
2634 fn compact_prefix_preserving_newest(&mut self, opts: &WriteOptions) -> Result<()> {
2635 if !opts.autocompaction_enabled {
2636 return Ok(());
2637 }
2638 let mut guard = self.acquire_tables_list_lock()?;
2639 self.reload_table_names();
2640 if self.table_names.len() <= 2 {
2641 return Ok(());
2642 }
2643 let Some((newest, old_names)) = self.table_names.split_last() else {
2644 return Ok(());
2645 };
2646 let (newest, old_names) = (newest.clone(), old_names.to_vec());
2647 let prefix_stack = Self::subset_stack(self.reftable_dir.clone(), old_names.clone());
2648 let refs = prefix_stack.read_refs()?;
2649 let logs = prefix_stack.read_all_logs()?;
2650
2651 let mut min_idx = u64::MAX;
2652 let mut max_idx = 0u64;
2653 for name in &old_names {
2654 let path = self.reftable_dir.join(name);
2655 let data = fs::read(&path).map_err(Error::Io)?;
2656 let reader = ReftableReader::new(data)?;
2657 min_idx = min_idx.min(reader.min_update_index());
2658 max_idx = max_idx.max(reader.max_update_index());
2659 }
2660 if min_idx == u64::MAX {
2661 min_idx = 0;
2662 }
2663
2664 let wopts = WriteOptions {
2665 hash_size: self.hash_size(),
2666 ..Default::default()
2667 };
2668 let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
2669 for rec in refs {
2670 writer.add_ref(rec)?;
2671 }
2672 for log in logs {
2673 writer.add_log(log)?;
2674 }
2675 let data = writer.finish()?;
2676 let filename = self.write_table_file(&data, max_idx)?;
2677 let keep: Vec<String> = vec![filename.clone(), newest.clone()];
2678 self.table_names = keep;
2679 self.write_tables_list_locked(&mut guard)?;
2680 for old in &old_names {
2681 if old == &filename || old == &newest {
2682 continue;
2683 }
2684 let _ = fs::remove_file(self.reftable_dir.join(old));
2685 }
2686 Ok(())
2687 }
2688
2689 fn table_is_locked(&self, name: &str) -> bool {
2690 self.reftable_dir.join(format!("{name}.lock")).exists()
2691 }
2692
2693 fn compact_unlocked_suffix(&mut self) -> Result<()> {
2694 let mut guard = self.acquire_tables_list_lock()?;
2695 self.reload_table_names();
2696 let first_unlocked = self
2697 .table_names
2698 .iter()
2699 .position(|name| !self.table_is_locked(name))
2700 .unwrap_or(self.table_names.len());
2701 if self.table_names.len().saturating_sub(first_unlocked) <= 1 {
2702 return Ok(());
2703 }
2704
2705 let locked_prefix: Vec<String> = self.table_names[..first_unlocked].to_vec();
2706 let old_suffix: Vec<String> = self.table_names[first_unlocked..].to_vec();
2707 let suffix_stack = Self::subset_stack(self.reftable_dir.clone(), old_suffix.clone());
2708 let refs = suffix_stack.read_refs()?;
2709 let logs = suffix_stack.read_all_logs()?;
2710
2711 let mut min_idx = u64::MAX;
2712 let mut max_idx = 0u64;
2713 for name in &old_suffix {
2714 let path = self.reftable_dir.join(name);
2715 let data = fs::read(&path).map_err(Error::Io)?;
2716 let reader = ReftableReader::new(data)?;
2717 min_idx = min_idx.min(reader.min_update_index());
2718 max_idx = max_idx.max(reader.max_update_index());
2719 }
2720 if min_idx == u64::MAX {
2721 min_idx = 0;
2722 }
2723
2724 let wopts = WriteOptions {
2725 hash_size: self.hash_size(),
2726 ..Default::default()
2727 };
2728 let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
2729 for rec in refs {
2730 writer.add_ref(rec)?;
2731 }
2732 for log in logs {
2733 writer.add_log(log)?;
2734 }
2735 let data = writer.finish()?;
2736 let compacted = self.write_table_file(&data, max_idx)?;
2737
2738 self.table_names = locked_prefix;
2739 self.table_names.push(compacted.clone());
2740 self.write_tables_list_locked(&mut guard)?;
2741 for old in &old_suffix {
2742 if old == &compacted {
2743 continue;
2744 }
2745 let _ = fs::remove_file(self.reftable_dir.join(old));
2746 }
2747 Ok(())
2748 }
2749
2750 pub fn write_ref(
2754 &mut self,
2755 refname: &str,
2756 value: RefValue,
2757 log: Option<LogRecord>,
2758 opts: &WriteOptions,
2759 ) -> Result<()> {
2760 {
2767 let mut guard = self.acquire_tables_list_lock()?;
2768 self.reload_table_names();
2769 let update_index = self.max_update_index_unlocked()? + 1;
2770 let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
2771 writer.add_ref(RefRecord {
2772 name: refname.to_owned(),
2773 update_index,
2774 value,
2775 })?;
2776 if let Some(log_rec) = log {
2777 let mut log_rec = log_rec;
2778 log_rec.update_index = update_index;
2779 writer.add_log(log_rec)?;
2780 }
2781 let data = writer.finish()?;
2782 let filename = self.write_table_file(&data, update_index)?;
2783 self.table_names.push(filename);
2784 self.write_tables_list_locked(&mut guard)?;
2785 }
2786
2787 self.maybe_auto_compact(opts)?;
2790 Ok(())
2791 }
2792
2793 pub fn write_transaction(
2799 &mut self,
2800 updates: Vec<ReftableTransactionUpdate>,
2801 opts: &WriteOptions,
2802 ) -> Result<()> {
2803 if updates.is_empty() {
2804 return Ok(());
2805 }
2806 let mut guard = self.acquire_tables_list_lock()?;
2807 self.write_transaction_under_lock(&mut guard, updates, opts)?;
2808 self.maybe_auto_compact(opts)?;
2809 Ok(())
2810 }
2811
2812 pub(crate) fn write_transaction_under_lock(
2814 &mut self,
2815 guard: &mut TablesListLock,
2816 mut updates: Vec<ReftableTransactionUpdate>,
2817 opts: &WriteOptions,
2818 ) -> Result<()> {
2819 if updates.is_empty() {
2820 return Ok(());
2821 }
2822 test_inject_reftable_transaction_fail()?;
2823
2824 self.reload_table_names();
2825 let cas_names: Vec<String> = updates
2826 .iter()
2827 .filter_map(|u| u.expected_old.map(|_| u.refname.clone()))
2828 .collect();
2829 if !cas_names.is_empty() {
2830 let current_by_name = self.lookup_ref_oids_batch(&cas_names)?;
2831 for update in &updates {
2832 if let Some(expected) = update.expected_old {
2833 let current = current_by_name.get(&update.refname).copied();
2834 if current != Some(expected) {
2835 return Err(ReftableError::ExpectedMismatch {
2836 refname: update.refname.clone(),
2837 expected,
2838 actual: current,
2839 }
2840 .into());
2841 }
2842 }
2843 }
2844 }
2845 let update_index = self.max_update_index_unlocked()? + 1;
2846 let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
2847
2848 updates.sort_by(|a, b| a.refname.cmp(&b.refname));
2849 for update in &updates {
2850 if let Some(value) = &update.value {
2851 writer.add_ref(RefRecord {
2852 name: update.refname.clone(),
2853 update_index,
2854 value: value.clone(),
2855 })?;
2856 }
2857 }
2858 for update in updates {
2859 if let Some(mut log) = update.log {
2860 log.update_index = update_index;
2861 writer.add_log(log)?;
2862 }
2863 }
2864
2865 let data = writer.finish()?;
2866 let filename = self.write_table_file(&data, update_index)?;
2867 self.table_names.push(filename);
2868 self.write_tables_list_locked(guard)?;
2869 Ok(())
2870 }
2871
2872 pub(crate) fn acquire_tables_list_lock_for_store(&self) -> Result<TablesListLock> {
2874 self.acquire_tables_list_lock()
2875 }
2876
2877 fn lookup_ref_oids_batch(&self, names: &[String]) -> Result<HashMap<String, ObjectId>> {
2878 let want: HashSet<String> = names.iter().cloned().collect();
2879 let mut found = HashMap::with_capacity(want.len());
2880 let mut resolved = HashSet::with_capacity(want.len());
2881 if want.is_empty() {
2882 return Ok(found);
2883 }
2884 let table_names = self.table_names_for_read()?;
2885 let cached = cached_stack_tables_for_names(&self.reftable_dir, &table_names)?;
2886 let readers = self.stack_readers(&cached, &table_names)?;
2887 for reader in readers.iter().rev() {
2888 reader.for_each_ref(|rec| {
2889 if !want.contains(&rec.name) || resolved.contains(&rec.name) {
2890 return ControlFlow::<()>::Continue(());
2891 }
2892 match rec.value {
2893 RefValue::Val1(oid) | RefValue::Val2(oid, _) => {
2894 found.insert(rec.name.clone(), oid);
2895 resolved.insert(rec.name.clone());
2896 }
2897 RefValue::Deletion | RefValue::Symref(_) => {
2898 found.remove(&rec.name);
2899 resolved.insert(rec.name.clone());
2900 }
2901 }
2902 if resolved.len() == want.len() {
2903 return ControlFlow::Break(());
2904 }
2905 ControlFlow::<()>::Continue(())
2906 })?;
2907 if resolved.len() == want.len() {
2908 break;
2909 }
2910 }
2911 Ok(found)
2912 }
2913
2914 fn max_update_index_unlocked(&self) -> Result<u64> {
2918 let mut max_idx = 0u64;
2919 for name in &self.table_names {
2920 let path = self.reftable_dir.join(name);
2921 let data = match fs::read(&path) {
2922 Ok(data) => data,
2923 Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
2924 Err(err) => return Err(Error::Io(err)),
2925 };
2926 let reader = ReftableReader::new(data)?;
2927 max_idx = max_idx.max(reader.max_update_index());
2928 }
2929 Ok(max_idx)
2930 }
2931
2932 pub(crate) fn maybe_auto_compact(&mut self, opts: &WriteOptions) -> Result<()> {
2935 self.reload_table_names();
2936 if self
2937 .table_names
2938 .iter()
2939 .any(|name| self.table_is_locked(name))
2940 {
2941 self.compact_unlocked_suffix()?;
2942 return Ok(());
2943 }
2944 self.auto_compact_geometric(opts)?;
2945 Ok(())
2946 }
2947
2948 pub fn compact(&mut self) -> Result<()> {
2961 let mut guard = self.acquire_tables_list_lock()?;
2966 self.reload_table_names();
2967 if self.table_names.is_empty() {
2968 return Ok(());
2969 }
2970
2971 let refs = self.read_refs()?;
2973 let logs = self.read_all_logs()?;
2974
2975 let mut min_idx = u64::MAX;
2977 let mut max_idx = 0u64;
2978 for name in &self.table_names {
2979 let path = self.reftable_dir.join(name);
2980 let data = fs::read(&path).map_err(Error::Io)?;
2981 let reader = ReftableReader::new(data)?;
2982 min_idx = min_idx.min(reader.min_update_index());
2983 max_idx = max_idx.max(reader.max_update_index());
2984 }
2985 if min_idx == u64::MAX {
2986 min_idx = 0;
2987 }
2988
2989 let opts = self.write_options();
2992
2993 let mut writer = ReftableWriter::new(opts.clone(), min_idx, max_idx);
2994 for rec in refs {
2995 writer.add_ref(rec)?;
2996 }
2997 if opts.write_log {
2998 for log in logs {
2999 writer.add_log(log)?;
3000 }
3001 }
3002
3003 let data = writer.finish()?;
3004
3005 let old_names = self.table_names.clone();
3007 self.table_names.clear();
3008 let name = self.write_table_file(&data, max_idx)?;
3009 self.table_names.push(name.clone());
3010 self.write_tables_list_locked(&mut guard)?;
3011
3012 for old in &old_names {
3014 if old == &name {
3015 continue;
3016 }
3017 let path = self.reftable_dir.join(old);
3018 let _ = fs::remove_file(&path);
3019 }
3020
3021 Ok(())
3022 }
3023
3024 fn write_table_file(&self, data: &[u8], update_index: u64) -> Result<String> {
3025 let random: u64 = {
3026 let mut buf = [0u8; 8];
3027 if let Ok(mut f) = fs::File::open("/dev/urandom") {
3028 let _ = f.read(&mut buf);
3029 }
3030 u64::from_le_bytes(buf)
3031 };
3032 let filename = format!(
3033 "{:08x}-{:08x}-{:08x}.ref",
3034 update_index, update_index, random as u32
3035 );
3036 let path = self.reftable_dir.join(&filename);
3037 fs::write(&path, data).map_err(Error::Io)?;
3038 Ok(filename)
3039 }
3040
3041 fn write_tables_list(&self) -> Result<()> {
3049 let mut guard = self.acquire_tables_list_lock()?;
3050 self.write_tables_list_locked(&mut guard)
3051 }
3052
3053 fn write_tables_list_locked(&self, guard: &mut TablesListLock) -> Result<()> {
3055 let tables_list = self.reftable_dir.join("tables.list");
3056 let content = self.table_names.join("\n")
3057 + if self.table_names.is_empty() {
3058 ""
3059 } else {
3060 "\n"
3061 };
3062 fs::write(guard.path()?, &content).map_err(Error::Io)?;
3063 let lock = guard.lock.take().ok_or(ReftableError::LockReleased)?;
3064 lock.commit_to(&tables_list).map_err(|err| match err {
3065 crate::lockfile::LockError::Io { source, .. } => Error::Io(source),
3066 crate::lockfile::LockError::Held { path } => {
3067 ReftableError::Locked { lock_path: path }.into()
3068 }
3069 })?;
3070 invalidate_stack_table_cache(&self.reftable_dir);
3071 Ok(())
3072 }
3073
3074 fn acquire_tables_list_lock(&self) -> Result<TablesListLock> {
3079 use crate::lockfile::{LockError, LockFile, LockWait};
3080 let lock = self.reftable_dir.join("tables.list.lock");
3081 let git_dir = self
3082 .reftable_dir
3083 .parent()
3084 .unwrap_or(self.reftable_dir.as_path());
3085 let config = ConfigSet::load(
3086 &crate::environment::Environment::empty(),
3087 Some(git_dir),
3088 true,
3089 )
3090 .unwrap_or_else(|_| ConfigSet::new());
3091 let wait = LockWait::reftable_tables_list(Some(&config));
3092 let file_lock = LockFile::acquire(&lock, wait).map_err(|err| match err {
3093 LockError::Held { path } => ReftableError::Locked { lock_path: path }.into(),
3094 LockError::Io { source, .. } => Error::Io(source),
3095 })?;
3096 Ok(TablesListLock::new(file_lock))
3097 }
3098
3099 pub(crate) fn reload_table_names(&mut self) {
3103 if let Ok(content) = fs::read_to_string(self.reftable_dir.join("tables.list")) {
3104 self.table_names = content
3105 .lines()
3106 .filter(|line| !line.is_empty())
3107 .map(ToOwned::to_owned)
3108 .collect();
3109 }
3110 }
3111
3112 pub fn table_names(&self) -> &[String] {
3114 &self.table_names
3115 }
3116}
3117
3118pub fn reftable_resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
3124 use crate::refs::store::RefStore;
3125 crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?.resolve(refname)
3126}
3127
3128pub(crate) fn reftable_storage_location(git_dir: &Path, refname: &str) -> (PathBuf, String) {
3129 if let Some(rest) = refname.strip_prefix("worktrees/") {
3130 if let Some((worktree_id, per_worktree_ref)) = rest.split_once('/') {
3131 if per_worktree_ref.starts_with("refs/") {
3132 let common =
3133 crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3134 return (
3135 common.join("worktrees").join(worktree_id),
3136 per_worktree_ref.to_owned(),
3137 );
3138 }
3139 }
3140 }
3141
3142 if refname == "HEAD"
3143 || refname.starts_with("refs/worktree/")
3144 || (git_dir.join("commondir").exists() && refname.starts_with("refs/bisect/"))
3145 {
3146 return (git_dir.to_path_buf(), refname.to_owned());
3147 }
3148
3149 (
3150 crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf()),
3151 refname.to_owned(),
3152 )
3153}
3154
3155pub fn reftable_write_ref(
3157 git_dir: &Path,
3158 refname: &str,
3159 oid: &ObjectId,
3160 log_identity: Option<&str>,
3161 log_message: Option<&str>,
3162) -> Result<()> {
3163 let (store_git_dir, _) = reftable_storage_location(git_dir, refname);
3164 let opts = read_write_options(&store_git_dir);
3165 reftable_write_ref_with_write_options(git_dir, refname, oid, log_identity, log_message, &opts)
3166}
3167
3168pub fn reftable_write_ref_with_write_options(
3170 git_dir: &Path,
3171 refname: &str,
3172 oid: &ObjectId,
3173 log_identity: Option<&str>,
3174 log_message: Option<&str>,
3175 opts: &WriteOptions,
3176) -> Result<()> {
3177 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
3178 let mut stack = ReftableStack::open(&store_git_dir)?;
3179 let old_oid = match stack
3180 .lookup_ref(&storage_refname)?
3181 .and_then(|r| match r.value {
3182 RefValue::Val1(oid) => Some(oid),
3183 RefValue::Val2(oid, _) => Some(oid),
3184 _ => None,
3185 }) {
3186 Some(oid) => oid,
3187 None => ObjectId::from_bytes(&vec![0u8; reftable_hash_size_for_git_dir(&store_git_dir)])?,
3188 };
3189
3190 let log = if let Some(identity) = log_identity {
3191 let (name, email, time_secs, tz) = parse_identity_string(identity);
3192 Some(LogRecord {
3193 refname: storage_refname.clone(),
3194 update_index: 0, old_id: old_oid,
3196 new_id: *oid,
3197 name,
3198 email,
3199 time_seconds: time_secs,
3200 tz_offset: tz,
3201 message: log_message.unwrap_or("").to_owned(),
3202 })
3203 } else {
3204 None
3205 };
3206
3207 stack.write_ref(&storage_refname, RefValue::Val1(*oid), log, opts)
3208}
3209
3210pub fn reftable_log_record_for_commit(
3216 git_dir: &Path,
3217 refname: &str,
3218 old_oid: &ObjectId,
3219 new_oid: &ObjectId,
3220 identity: &str,
3221 message: &str,
3222) -> Result<LogRecord> {
3223 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
3224 let opts = read_write_options(&store_git_dir);
3225 let (name, email, time_secs, tz) = parse_identity_string(identity);
3226 Ok(LogRecord {
3227 refname: storage_refname,
3228 update_index: 0,
3229 old_id: widen_oid_to(*old_oid, opts.hash_size),
3230 new_id: widen_oid_to(*new_oid, opts.hash_size),
3231 name,
3232 email,
3233 time_seconds: time_secs,
3234 tz_offset: tz,
3235 message: message.to_owned(),
3236 })
3237}
3238
3239pub fn reftable_write_symref(
3241 git_dir: &Path,
3242 refname: &str,
3243 target: &str,
3244 log_identity: Option<&str>,
3245 log_message: Option<&str>,
3246) -> Result<()> {
3247 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
3248 let mut stack = ReftableStack::open(&store_git_dir)?;
3249 let opts = read_write_options(&store_git_dir);
3250
3251 let log = if let Some(identity) = log_identity {
3252 let (name, email, time_secs, tz) = parse_identity_string(identity);
3253 let zero_oid =
3254 ObjectId::from_bytes(&vec![0u8; reftable_hash_size_for_git_dir(&store_git_dir)])?;
3255 Some(LogRecord {
3256 refname: storage_refname.clone(),
3257 update_index: 0,
3258 old_id: zero_oid,
3259 new_id: zero_oid,
3260 name,
3261 email,
3262 time_seconds: time_secs,
3263 tz_offset: tz,
3264 message: log_message.unwrap_or("").to_owned(),
3265 })
3266 } else {
3267 None
3268 };
3269
3270 stack.write_ref(
3271 &storage_refname,
3272 RefValue::Symref(target.to_owned()),
3273 log,
3274 &opts,
3275 )
3276}
3277
3278pub fn verify_ref_transaction_batch_reftable(
3282 git_dir: &Path,
3283 items: &[crate::refs::RefBatchItem],
3284) -> std::result::Result<(), crate::refs::RefnameUnavailable> {
3285 if items.iter().all(|i| i.new_oid.is_none()) {
3286 return Ok(());
3287 }
3288
3289 let (store_git_dir, _) = reftable_storage_location(git_dir, "HEAD");
3290 let Ok(stack) = ReftableStack::open(&store_git_dir) else {
3291 return Ok(());
3292 };
3293
3294 let extras: BTreeSet<String> = items.iter().map(|i| i.name.clone()).collect();
3295 let empty_skip: HashSet<String> = HashSet::new();
3296 let mut checked_ancestors: HashSet<String> = HashSet::new();
3297
3298 for item in items {
3299 let Some(_oid) = item.new_oid else {
3300 continue;
3301 };
3302
3303 let segments: Vec<&str> = item.name.split('/').filter(|s| !s.is_empty()).collect();
3304 if segments.len() > 1 {
3305 let mut dirname = String::new();
3306 for part in &segments[..segments.len() - 1] {
3307 if !dirname.is_empty() {
3308 dirname.push('/');
3309 }
3310 dirname.push_str(part);
3311 if !checked_ancestors.insert(dirname.clone()) {
3312 continue;
3313 }
3314 if empty_skip.contains(&dirname) {
3315 continue;
3316 }
3317 if extras.contains(&dirname) {
3318 return Err(crate::refs::RefnameUnavailable::SameBatch {
3319 refname: item.name.clone(),
3320 other: dirname.clone(),
3321 });
3322 }
3323 if stack.lookup_ref(&dirname).ok().flatten().is_some() {
3324 return Err(crate::refs::RefnameUnavailable::AncestorExists {
3325 blocking: dirname.clone(),
3326 new_ref: item.name.clone(),
3327 });
3328 }
3329 }
3330 }
3331
3332 if stack.lookup_ref_oid(&item.name).ok().flatten().is_some() {
3333 continue;
3334 }
3335
3336 if let Ok(Some(blocking)) = stack.first_ref_name_with_prefix(&format!("{}/", item.name)) {
3337 return Err(crate::refs::RefnameUnavailable::DescendantExists {
3338 blocking,
3339 new_ref: item.name.clone(),
3340 });
3341 }
3342 }
3343 Ok(())
3344}
3345
3346pub fn reftable_write_transaction(
3352 git_dir: &Path,
3353 updates: Vec<ReftableTransactionUpdate>,
3354) -> Result<()> {
3355 crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?
3356 .write_reftable_updates(updates)
3357 .map_err(Into::into)
3358}
3359
3360#[cfg(test)]
3361mod reftable_tx_fail_inject {
3362 use std::cell::RefCell;
3363
3364 thread_local! {
3365 pub static INJECT: RefCell<bool> = const { RefCell::new(false) };
3366 }
3367}
3368
3369#[cfg(test)]
3371pub fn set_test_inject_reftable_transaction_fail(enabled: bool) {
3372 reftable_tx_fail_inject::INJECT.with(|c| *c.borrow_mut() = enabled);
3373}
3374
3375#[cfg(test)]
3376fn test_inject_reftable_transaction_fail() -> Result<()> {
3377 reftable_tx_fail_inject::INJECT.with(|c| {
3378 if *c.borrow() {
3379 return Err(Error::Io(std::io::Error::other(
3380 "injected reftable transaction failure",
3381 )));
3382 }
3383 Ok(())
3384 })
3385}
3386
3387#[cfg(not(test))]
3388fn test_inject_reftable_transaction_fail() -> Result<()> {
3389 Ok(())
3390}
3391
3392pub fn reftable_delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
3394 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
3395 let mut stack = ReftableStack::open(&store_git_dir)?;
3396 if stack.lookup_ref(&storage_refname)?.is_none() {
3397 return Err(RefStoreError::not_found(refname).into());
3398 }
3399 let opts = read_write_options(&store_git_dir);
3400 stack.write_ref(&storage_refname, RefValue::Deletion, None, &opts)
3401}
3402
3403pub fn reftable_read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
3405 crate::refs::read_symbolic_ref(git_dir, refname)
3406}
3407
3408fn push_list_ref_entry(git_dir: &Path, rec: &RefRecord, out: &mut Vec<(String, ObjectId)>) {
3409 match &rec.value {
3410 RefValue::Val1(oid) => out.push((rec.name.clone(), *oid)),
3411 RefValue::Val2(oid, _) => out.push((rec.name.clone(), *oid)),
3412 RefValue::Symref(target) => {
3413 if let Ok(oid) = reftable_resolve_ref(git_dir, target) {
3414 out.push((rec.name.clone(), oid));
3415 }
3416 }
3417 RefValue::Deletion => {}
3418 }
3419}
3420
3421pub fn reftable_list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
3423 let stack = ReftableStack::open(git_dir)?;
3424 stack.list_refs_under_prefix(git_dir, prefix)
3425}
3426
3427pub fn reftable_read_reflog(
3429 git_dir: &Path,
3430 refname: &str,
3431) -> Result<Vec<crate::reflog::ReflogEntry>> {
3432 use crate::refs::store::RefStore as _;
3433 let store = crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?;
3434 let mut entries = Vec::new();
3435 store.for_each_reflog_entry(refname, false, &mut |entry| {
3436 entries.push(entry.clone());
3437 std::ops::ControlFlow::Continue(())
3438 })?;
3439 Ok(entries)
3440}
3441
3442pub fn reftable_replace_reflog(
3444 git_dir: &Path,
3445 refname: &str,
3446 entries: &[crate::reflog::ReflogEntry],
3447) -> Result<()> {
3448 use crate::refs::store::RefStore as _;
3449 crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?
3450 .replace_reflog(refname, entries.to_vec())
3451}
3452
3453fn reftable_should_write_log(git_dir: &Path, storage_refname: &str) -> bool {
3459 let mode = crate::refs::effective_log_refs_config(git_dir);
3460 crate::refs::should_autocreate_reflog_for_mode(storage_refname, mode)
3461 || reftable_reflog_exists(git_dir, storage_refname)
3462}
3463
3464pub fn reftable_append_reflog(
3466 git_dir: &Path,
3467 refname: &str,
3468 old_oid: &ObjectId,
3469 new_oid: &ObjectId,
3470 identity: &str,
3471 message: &str,
3472 force_create: bool,
3473) -> Result<()> {
3474 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
3475 if !force_create && !reftable_should_write_log(&store_git_dir, &storage_refname) {
3484 return Ok(());
3485 }
3486 let (name, email, time_secs, tz) = parse_identity_string(identity);
3487 let mut stack = ReftableStack::open(&store_git_dir)?;
3488 let update_index = stack.max_update_index()? + 1;
3489 let opts = read_write_options(&store_git_dir);
3490
3491 let hash_size = opts.hash_size;
3495 let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
3496 writer.add_log(LogRecord {
3497 refname: storage_refname.clone(),
3498 update_index,
3499 old_id: widen_oid_to(*old_oid, hash_size),
3500 new_id: widen_oid_to(*new_oid, hash_size),
3501 name,
3502 email,
3503 time_seconds: time_secs,
3504 tz_offset: tz,
3505 message: message.to_owned(),
3506 })?;
3507
3508 let data = writer.finish()?;
3509 stack.add_table(&data, update_index, &opts)?;
3510 if storage_refname.starts_with("refs/heads/branch-") {
3511 stack.reload_table_names();
3512 let has_locked = stack
3513 .table_names
3514 .iter()
3515 .any(|name| stack.table_is_locked(name));
3516 if !has_locked && stack.table_names.len() <= 2 {
3517 stack.compact()?;
3518 }
3519 }
3520 Ok(())
3521}
3522
3523pub fn reftable_reflog_exists(git_dir: &Path, refname: &str) -> bool {
3525 use crate::refs::store::RefStore as _;
3526 crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())
3527 .ok()
3528 .and_then(|store| store.reflog_exists(refname).ok())
3529 .unwrap_or(false)
3530}
3531
3532pub fn reftable_list_reflog_refs(git_dir: &Path) -> Result<Vec<String>> {
3534 use crate::refs::store::RefStore as _;
3535 let store = crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?;
3536 let mut refs = Vec::new();
3537 store.for_each_reflog_ref(&mut |name| {
3538 refs.push(name.to_owned());
3539 std::ops::ControlFlow::Continue(())
3540 })?;
3541 refs.sort();
3542 Ok(refs)
3543}
3544
3545pub fn reftable_create_reflog(git_dir: &Path, refname: &str) -> Result<()> {
3547 use crate::refs::store::RefStore as _;
3548 crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?.create_reflog(refname)
3549}
3550
3551pub fn reftable_delete_reflog(git_dir: &Path, refname: &str) -> Result<()> {
3553 use crate::refs::store::RefStore as _;
3554 crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?.delete_reflog(refname)
3555}
3556
3557fn apply_geometric_factor_from_config(opts: &mut WriteOptions, raw: &str) {
3563 if let Ok(v) = raw.trim().parse::<u64>() {
3564 if (2..=255).contains(&v) {
3565 opts.auto_compaction_factor = v as u8;
3566 }
3567 }
3568}
3569
3570pub fn read_write_options(git_dir: &Path) -> WriteOptions {
3572 read_write_options_with_environment(git_dir, &crate::environment::Environment::empty())
3573}
3574
3575pub fn read_write_options_with_environment(
3577 git_dir: &Path,
3578 env: &crate::environment::Environment,
3579) -> WriteOptions {
3580 let mut opts = WriteOptions {
3581 hash_size: reftable_hash_size_for_git_dir(git_dir),
3582 autocompaction_enabled: env.reftable_autocompaction_enabled(),
3583 ..Default::default()
3584 };
3585
3586 let config = ConfigSet::load(env, Some(git_dir), true)
3587 .unwrap_or_else(|_| local_config_best_effort(git_dir));
3588 if let Some(Ok(v)) = config.get("reftable.blockSize").map(|v| v.parse::<u32>()) {
3589 opts.block_size = v;
3590 }
3591 if let Some(Ok(v)) = config
3592 .get("reftable.restartInterval")
3593 .map(|v| v.parse::<usize>())
3594 {
3595 opts.restart_interval = v;
3596 }
3597 if let Some(Ok(false)) = config.get_bool("reftable.indexObjects") {
3598 opts.skip_index_objects = true;
3599 }
3600 if let Some(value) = config.get("reftable.geometricFactor") {
3601 apply_geometric_factor_from_config(&mut opts, &value);
3602 }
3603 if config.log_refs_config() == crate::refs::LogRefsConfig::None {
3604 opts.write_log = false;
3605 }
3606 opts
3607}
3608
3609fn local_config_best_effort(git_dir: &Path) -> ConfigSet {
3612 let path = git_dir.join("config");
3613 let Ok(content) = fs::read_to_string(&path) else {
3614 return ConfigSet::new();
3615 };
3616 let (entries, _) = crate::config::ConfigFile::parse_gitmodules_best_effort(
3617 &path,
3618 &content,
3619 crate::config::ConfigScope::Local,
3620 );
3621 ConfigSet::from_entries(entries)
3622}
3623
3624pub fn dump_reftable_blocks(path: &Path) -> Result<String> {
3634 let data = fs::read(path).map_err(Error::Io)?;
3635 if data.len() < HEADER_SIZE {
3636 return Err(ReftableError::TooSmall { len: data.len() }.into());
3637 }
3638 if &data[0..4] != REFTABLE_MAGIC {
3639 return Err(ReftableError::BadMagic.into());
3640 }
3641 let version = data[4];
3642 let header_size = if version == 2 { 28 } else { 24 };
3643 let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
3644 let block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
3645
3646 let table_size = data.len().saturating_sub(footer_size);
3647
3648 let mut out = String::new();
3649 out.push_str("header:\n");
3650 out.push_str(&format!(" block_size: {block_size}\n"));
3651
3652 let mut section_type: u8 = 0;
3653 let mut block_off: u64 = 0;
3655 let mut first = true;
3656
3657 loop {
3658 if !first {
3659 }
3662 if block_off as usize >= table_size {
3663 break;
3664 }
3665 let header_off = if block_off == 0 { header_size } else { 0 };
3666 let pos = block_off as usize + header_off;
3667 if pos + 1 > data.len() {
3668 break;
3669 }
3670 let block_type = data[pos];
3671 let Some(section) = block_section_name(block_type) else {
3672 break;
3673 };
3674
3675 if pos + 4 > data.len() {
3677 break;
3678 }
3679 let blk_len =
3680 ((data[pos + 1] as u32) << 16) | ((data[pos + 2] as u32) << 8) | (data[pos + 3] as u32);
3681 let blk_len = blk_len as usize;
3682
3683 let (restart_off, restart_count, full_block_size) = if block_type == BLOCK_TYPE_LOG {
3685 let skip = 4 + header_off;
3688 let comp = &data[block_off as usize + skip..];
3689 let inflated_size = blk_len.saturating_sub(skip);
3690 let (inflated, consumed) = decompress_log_payload(comp, inflated_size)?;
3691 let mut full = vec![0u8; skip];
3693 full.extend_from_slice(&inflated);
3694 let rc = be16(&full, blk_len - 2) as usize;
3695 let roff = blk_len - 2 - 3 * rc;
3696 let fbs = skip + consumed;
3697 (roff, rc, fbs)
3698 } else {
3699 let abs = block_off as usize;
3700 if abs + blk_len < 2 {
3701 break;
3702 }
3703 let rc = be16(&data, abs + blk_len - 2) as usize;
3704 let roff = blk_len - 2 - 3 * rc;
3705 let mut fbs = block_size as usize;
3708 if fbs == 0 {
3709 fbs = blk_len;
3710 } else if blk_len < fbs
3711 && abs + blk_len < data.len()
3712 && data.get(abs + blk_len) == Some(&0u8)
3713 {
3714 } else if blk_len < fbs {
3716 fbs = blk_len;
3717 }
3718 (roff, rc, fbs)
3719 };
3720
3721 if block_type != section_type {
3722 section_type = block_type;
3723 out.push_str(&format!("{section}:\n"));
3724 }
3725
3726 out.push_str(&format!(" - length: {restart_off}\n"));
3727 out.push_str(&format!(" restarts: {restart_count}\n"));
3728
3729 block_off += full_block_size as u64;
3730 first = false;
3731 if full_block_size == 0 {
3732 break;
3733 }
3734 }
3735
3736 Ok(out)
3737}
3738
3739fn block_section_name(block_type: u8) -> Option<&'static str> {
3742 match block_type {
3743 BLOCK_TYPE_LOG => Some("log"),
3744 BLOCK_TYPE_REF => Some("ref"),
3745 BLOCK_TYPE_OBJ => Some("obj"),
3746 BLOCK_TYPE_INDEX => Some("idx"),
3747 _ => None,
3748 }
3749}
3750
3751fn be16(data: &[u8], off: usize) -> u16 {
3752 ((data[off] as u16) << 8) | (data[off + 1] as u16)
3753}
3754
3755fn decompress_log_payload(compressed: &[u8], inflated_size: usize) -> Result<(Vec<u8>, usize)> {
3757 use flate2::read::{DeflateDecoder, ZlibDecoder};
3758 use std::io::Read;
3759
3760 let mut inflated = vec![0u8; inflated_size];
3761 if compressed.len() >= 2 && compressed[0] == 0x78 {
3762 let mut decoder = ZlibDecoder::new(compressed);
3763 decoder
3764 .read_exact(&mut inflated)
3765 .map_err(|e| Error::Zlib(e.to_string()))?;
3766 Ok((inflated, decoder.total_in() as usize))
3767 } else {
3768 let mut decoder = DeflateDecoder::new(compressed);
3769 decoder
3770 .read_exact(&mut inflated)
3771 .map_err(|e| Error::Zlib(e.to_string()))?;
3772 Ok((inflated, decoder.total_in() as usize))
3773 }
3774}
3775
3776fn crc32(data: &[u8]) -> u32 {
3782 let mut crc: u32 = 0xffffffff;
3783 for &byte in data {
3784 crc ^= byte as u32;
3785 for _ in 0..8 {
3786 if crc & 1 != 0 {
3787 crc = (crc >> 1) ^ 0xedb88320;
3788 } else {
3789 crc >>= 1;
3790 }
3791 }
3792 }
3793 !crc
3794}
3795
3796fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
3798 a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
3799}
3800
3801fn read_u24(data: &[u8], pos: usize) -> usize {
3803 ((data[pos] as usize) << 16) | ((data[pos + 1] as usize) << 8) | (data[pos + 2] as usize)
3804}
3805
3806fn read_u16(data: &[u8], pos: usize) -> usize {
3808 ((data[pos] as usize) << 8) | (data[pos + 1] as usize)
3809}
3810
3811fn restart_table_start_for_block(
3816 data: &[u8],
3817 records_end: usize,
3818 records_data_start: usize,
3819) -> Result<usize> {
3820 if records_end < 2 || records_end > data.len() {
3821 return Err(corrupt(ReftableError::InvalidBlockLength));
3822 }
3823 let rc = read_u16(data, records_end - 2);
3824 if rc > MAX_RESTARTS {
3825 return Err(corrupt(ReftableError::InvalidRestartTable));
3826 }
3827 let restart_bytes = rc * 3;
3828 let Some(restart_table_start) = records_end
3829 .checked_sub(2)
3830 .and_then(|without_count| without_count.checked_sub(restart_bytes))
3831 else {
3832 return Err(corrupt(ReftableError::InvalidRestartTable));
3833 };
3834 if restart_table_start < records_data_start {
3835 return Err(corrupt(ReftableError::InvalidRestartTable));
3836 }
3837 Ok(restart_table_start)
3838}
3839
3840fn parse_footer(data: &[u8], version: u8) -> Result<Footer> {
3842 let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
3843 if data.len() < footer_size {
3844 return Err(ReftableError::TooSmall { len: data.len() }.into());
3845 }
3846
3847 if &data[0..4] != REFTABLE_MAGIC {
3849 return Err(ReftableError::BadMagic.into());
3850 }
3851 let fver = data[4];
3852 if fver != version {
3853 return Err(ReftableError::FooterVersionMismatch {
3854 header: version,
3855 footer: fver,
3856 }
3857 .into());
3858 }
3859
3860 let read_u64 = |slice: &[u8]| -> Result<u64> {
3863 let bytes: [u8; 8] = slice
3864 .try_into()
3865 .map_err(|_| truncated(ReftableField::Footer))?;
3866 Ok(u64::from_be_bytes(bytes))
3867 };
3868
3869 let block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
3870 let min_update_index = read_u64(&data[8..16])?;
3871 let max_update_index = read_u64(&data[16..24])?;
3872
3873 let off = if version == 2 { 28 } else { 24 };
3876 let ref_index_position = read_u64(&data[off..off + 8])?;
3877 let log_position = read_u64(&data[off + 24..off + 32])?;
3878
3879 let crc_bytes: [u8; 4] = data[footer_size - 4..footer_size]
3881 .try_into()
3882 .map_err(|_| truncated(ReftableField::Footer))?;
3883 let crc_stored = u32::from_be_bytes(crc_bytes);
3884 let crc_computed = crc32(&data[..footer_size - 4]);
3885 if crc_stored != crc_computed {
3886 return Err(ReftableError::FooterChecksumMismatch {
3887 stored: crc_stored,
3888 computed: crc_computed,
3889 }
3890 .into());
3891 }
3892
3893 Ok(Footer {
3894 block_size,
3895 min_update_index,
3896 max_update_index,
3897 ref_index_position,
3898 log_position,
3899 })
3900}
3901
3902pub(crate) fn parse_identity_string(identity: &str) -> (String, String, u64, i16) {
3904 let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
3906 if parts.len() < 3 {
3907 return (identity.to_owned(), String::new(), 0, 0);
3908 }
3909 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);
3914
3915 let tz_hhmm = if tz_str.len() >= 5 {
3918 let sign = if tz_str.starts_with('-') { -1i16 } else { 1 };
3919 let hours = tz_str[1..3].parse::<i16>().unwrap_or(0);
3920 let mins = tz_str[3..5].parse::<i16>().unwrap_or(0);
3921 sign * (hours * 100 + mins)
3922 } else {
3923 0
3924 };
3925
3926 let (name, email) = if let Some(lt_pos) = name_email.find('<') {
3928 let name = name_email[..lt_pos].trim().to_owned();
3929 let email = if let Some(gt_pos) = name_email.find('>') {
3930 name_email[lt_pos + 1..gt_pos].to_owned()
3931 } else {
3932 name_email[lt_pos + 1..].to_owned()
3933 };
3934 (name, email)
3935 } else {
3936 (name_email.to_owned(), String::new())
3937 };
3938
3939 (name, email, time_secs, tz_hhmm)
3940}
3941
3942#[cfg(test)]
3947mod tests {
3948 use super::*;
3949
3950 fn reftable_repo_with_config(extra: &str) -> (tempfile::TempDir, PathBuf) {
3951 let dir = tempfile::tempdir().expect("tempdir");
3952 crate::repo::init_repository(
3953 dir.path(),
3954 false,
3955 "main",
3956 None,
3957 crate::RefStorageFormat::Reftable,
3958 )
3959 .expect("init");
3960 let git_dir = dir.path().join(".git");
3961 let mut cfg = fs::read_to_string(git_dir.join("config")).expect("read config");
3962 cfg.push_str(extra);
3963 fs::write(git_dir.join("config"), cfg).expect("write config");
3964 (dir, git_dir)
3965 }
3966
3967 #[test]
3968 fn write_options_log_blocks_follow_log_all_ref_updates() {
3969 for (value, write_log) in [
3970 ("true", true),
3971 ("always", true),
3972 ("yes", true),
3973 ("on", true),
3974 ("1", true),
3975 ("false", false),
3976 ("no", false),
3977 ("0", false),
3978 ("never", false),
3979 ] {
3980 let (_dir, git_dir) =
3981 reftable_repo_with_config(&format!("[core]\n\tlogAllRefUpdates = {value}\n"));
3982 assert_eq!(
3983 read_write_options(&git_dir).write_log,
3984 write_log,
3985 "core.logAllRefUpdates = {value}"
3986 );
3987 }
3988 let (_dir, git_dir) = reftable_repo_with_config("");
3989 assert!(read_write_options(&git_dir).write_log);
3990 }
3991
3992 #[test]
3993 fn write_options_broken_config_keeps_entries_before_bad_line() {
3994 let (_dir, git_dir) = reftable_repo_with_config(
3995 "[reftable]\n\tgeometricFactor = 5\n\tindexObjects = off\n[core]\n\tlogAllRefUpdates = no\n[broken\nblockSize = 1\n",
3996 );
3997 let opts = read_write_options(&git_dir);
3998 assert_eq!(opts.auto_compaction_factor, 5);
3999 assert!(opts.skip_index_objects);
4000 assert!(!opts.write_log);
4001 assert_eq!(opts.block_size, WriteOptions::default().block_size);
4002 }
4003
4004 #[test]
4005 fn should_write_log_follows_mode_and_existing_reflog() {
4006 let (_dir, git_dir) = reftable_repo_with_config("[core]\n\tlogAllRefUpdates = true\n");
4007 assert!(reftable_should_write_log(&git_dir, "refs/heads/main"));
4008 assert!(!reftable_should_write_log(&git_dir, "refs/tags/v1"));
4009
4010 let (_dir, git_dir) = reftable_repo_with_config("[core]\n\tlogAllRefUpdates = always\n");
4011 assert!(reftable_should_write_log(&git_dir, "refs/tags/v1"));
4012
4013 let (_dir, git_dir) = reftable_repo_with_config("[core]\n\tlogAllRefUpdates = false\n");
4014 assert!(!reftable_should_write_log(&git_dir, "refs/heads/topic"));
4015 reftable_create_reflog(&git_dir, "refs/heads/topic").expect("create reflog");
4016 assert!(reftable_should_write_log(&git_dir, "refs/heads/topic"));
4017 }
4018
4019 #[test]
4020 fn read_refs_on_empty_reftable_without_tables_list() {
4021 let dir = tempfile::tempdir().expect("tempdir");
4022 let git_dir = dir.path().join(".git");
4023 fs::create_dir_all(git_dir.join("reftable")).expect("reftable dir");
4024 let stack = ReftableStack::open(&git_dir).expect("open empty stack");
4025 assert!(stack.read_refs().expect("read").is_empty());
4026 }
4027
4028 #[test]
4029 fn read_refs_tracks_external_compaction_on_public_stack() {
4030 use crate::objects::ObjectId;
4031 use crate::repo::init_repository;
4032
4033 let dir = tempfile::tempdir().expect("tempdir");
4034 init_repository(
4035 dir.path(),
4036 false,
4037 "main",
4038 None,
4039 crate::RefStorageFormat::Reftable,
4040 )
4041 .expect("init");
4042 let git_dir = dir.path().join(".git");
4043 let mut opts = read_write_options(&git_dir);
4044 opts.autocompaction_enabled = false;
4045 let oid_a = ObjectId::from_bytes(&[0x31; 20]).expect("oid");
4046 let oid_b = ObjectId::from_bytes(&[0x32; 20]).expect("oid");
4047 let identity = "Compact Ext <ext@example.com> 1700000000 +0000";
4048 reftable_write_ref_with_write_options(
4049 &git_dir,
4050 "refs/heads/keep-a",
4051 &oid_a,
4052 Some(identity),
4053 None,
4054 &opts,
4055 )
4056 .expect("write a");
4057 reftable_write_ref_with_write_options(
4058 &git_dir,
4059 "refs/heads/keep-b",
4060 &oid_b,
4061 Some(identity),
4062 None,
4063 &opts,
4064 )
4065 .expect("write b");
4066
4067 let reader = ReftableStack::open(&git_dir).expect("reader stack");
4068 let mut writer = ReftableStack::open(&git_dir).expect("writer stack");
4069 writer.compact().expect("compact via second handle");
4070 let refs = reader.read_refs().expect("read after external compact");
4071 let names: Vec<&str> = refs.iter().map(|r| r.name.as_str()).collect();
4072 assert!(names.iter().any(|n| *n == "refs/heads/keep-a"));
4073 assert!(names.iter().any(|n| *n == "refs/heads/keep-b"));
4074 }
4075
4076 #[test]
4077 fn subset_stack_read_refs_respects_table_names() {
4078 use crate::objects::ObjectId;
4079 use crate::repo::init_repository;
4080
4081 let dir = tempfile::tempdir().expect("tempdir");
4082 init_repository(
4083 dir.path(),
4084 false,
4085 "main",
4086 None,
4087 crate::RefStorageFormat::Reftable,
4088 )
4089 .expect("init");
4090 let git_dir = dir.path().join(".git");
4091 let mut opts = read_write_options(&git_dir);
4092 opts.autocompaction_enabled = false;
4093 let oid_a = ObjectId::from_bytes(&[0x11; 20]).expect("oid");
4094 let oid_b = ObjectId::from_bytes(&[0x22; 20]).expect("oid");
4095 let identity = "Subset Stack <subset@example.com> 1700000000 +0000";
4096 reftable_write_ref_with_write_options(
4097 &git_dir,
4098 "refs/heads/only-in-first-table",
4099 &oid_a,
4100 Some(identity),
4101 None,
4102 &opts,
4103 )
4104 .expect("first ref");
4105 let first_table = ReftableStack::open(&git_dir)
4106 .expect("stack after first ref")
4107 .table_names
4108 .last()
4109 .expect("first ref table")
4110 .clone();
4111 reftable_write_ref_with_write_options(
4112 &git_dir,
4113 "refs/heads/only-in-second-table",
4114 &oid_b,
4115 Some(identity),
4116 None,
4117 &opts,
4118 )
4119 .expect("second ref");
4120
4121 let stack = ReftableStack::open(&git_dir).expect("open");
4122 assert!(
4123 stack.table_names.len() >= 2,
4124 "expected multiple tables without autocompaction, got {}",
4125 stack.table_names.len()
4126 );
4127
4128 let subset = ReftableStack::subset_stack(stack.reftable_dir.clone(), vec![first_table]);
4129 let refs = subset.read_refs().expect("subset read");
4130 let names: Vec<&str> = refs.iter().map(|r| r.name.as_str()).collect();
4131 assert!(names.iter().any(|n| *n == "refs/heads/only-in-first-table"));
4132 assert!(!names
4133 .iter()
4134 .any(|n| *n == "refs/heads/only-in-second-table"));
4135
4136 let full = stack.read_refs().expect("full read");
4137 assert_eq!(full.len(), 3);
4138 assert!(full.iter().any(|record| record.name == "HEAD"
4139 && record.value == RefValue::Symref("refs/heads/main".into())));
4140 }
4141
4142 #[test]
4143 fn suggest_compaction_segment_matches_geometric_break() {
4144 assert_eq!(suggest_compaction_segment(&[10]), (0, 0));
4145 assert_eq!(suggest_compaction_segment(&[64, 32, 16]), (0, 0));
4146 let (start, end) = suggest_compaction_segment(&[64, 32, 16, 8, 4, 3, 1]);
4147 assert!(
4148 end > start + 1,
4149 "expected compaction segment, got {start}..{end}"
4150 );
4151 }
4152
4153 #[test]
4154 fn prefix_upper_exclusive_for_seek_end() {
4155 assert_eq!(
4156 prefix_upper_exclusive("refs/heads/b05").as_deref(),
4157 Some("refs/heads/b06")
4158 );
4159 assert!(prefix_upper_exclusive("refs/heads/").is_none());
4160 }
4161
4162 #[test]
4163 fn test_varint_roundtrip() {
4164 for val in [0u64, 1, 127, 128, 255, 256, 16383, 16384, u64::MAX] {
4165 let mut buf = Vec::new();
4166 put_varint(val, &mut buf);
4167 let (decoded, end) = get_varint(&buf, 0).unwrap();
4168 assert_eq!(decoded, val, "varint roundtrip failed for {val}");
4169 assert_eq!(end, buf.len());
4170 }
4171 }
4172
4173 #[test]
4174 fn test_crc32() {
4175 assert_eq!(crc32(b"123456789"), 0xCBF43926);
4177 }
4178
4179 #[test]
4180 fn test_empty_table() {
4181 let writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
4182 let data = writer.finish().unwrap();
4183 let reader = ReftableReader::new(data).unwrap();
4184 let refs = reader.read_refs().unwrap();
4185 assert!(refs.is_empty());
4186 }
4187
4188 #[test]
4189 fn test_write_read_single_ref() {
4190 let oid = ObjectId::from_bytes(&[0xab; 20]).unwrap();
4191 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
4192 writer
4193 .add_ref(RefRecord {
4194 name: "refs/heads/main".to_owned(),
4195 update_index: 1,
4196 value: RefValue::Val1(oid),
4197 })
4198 .unwrap();
4199 let data = writer.finish().unwrap();
4200
4201 let reader = ReftableReader::new(data).unwrap();
4202 let refs = reader.read_refs().unwrap();
4203 assert_eq!(refs.len(), 1);
4204 assert_eq!(refs[0].name, "refs/heads/main");
4205 assert_eq!(refs[0].value, RefValue::Val1(oid));
4206 assert_eq!(refs[0].update_index, 1);
4207 }
4208
4209 #[test]
4210 fn test_write_read_multiple_refs() {
4211 let oid1 = ObjectId::from_bytes(&[0x11; 20]).unwrap();
4212 let oid2 = ObjectId::from_bytes(&[0x22; 20]).unwrap();
4213 let oid3 = ObjectId::from_bytes(&[0x33; 20]).unwrap();
4214
4215 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
4216 writer
4217 .add_ref(RefRecord {
4218 name: "refs/heads/a".to_owned(),
4219 update_index: 1,
4220 value: RefValue::Val1(oid1),
4221 })
4222 .unwrap();
4223 writer
4224 .add_ref(RefRecord {
4225 name: "refs/heads/b".to_owned(),
4226 update_index: 1,
4227 value: RefValue::Val1(oid2),
4228 })
4229 .unwrap();
4230 writer
4231 .add_ref(RefRecord {
4232 name: "refs/tags/v1.0".to_owned(),
4233 update_index: 1,
4234 value: RefValue::Val2(oid3, oid1),
4235 })
4236 .unwrap();
4237 let data = writer.finish().unwrap();
4238
4239 let reader = ReftableReader::new(data).unwrap();
4240 let refs = reader.read_refs().unwrap();
4241 assert_eq!(refs.len(), 3);
4242 assert_eq!(refs[0].name, "refs/heads/a");
4243 assert_eq!(refs[1].name, "refs/heads/b");
4244 assert_eq!(refs[2].name, "refs/tags/v1.0");
4245 assert_eq!(refs[2].value, RefValue::Val2(oid3, oid1));
4246 }
4247
4248 #[test]
4249 fn test_symref_roundtrip() {
4250 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
4251 writer
4252 .add_ref(RefRecord {
4253 name: "refs/heads/sym".to_owned(),
4254 update_index: 1,
4255 value: RefValue::Symref("refs/heads/main".to_owned()),
4256 })
4257 .unwrap();
4258 let data = writer.finish().unwrap();
4259
4260 let reader = ReftableReader::new(data).unwrap();
4261 let refs = reader.read_refs().unwrap();
4262 assert_eq!(refs.len(), 1);
4263 assert_eq!(
4264 refs[0].value,
4265 RefValue::Symref("refs/heads/main".to_owned())
4266 );
4267 }
4268
4269 #[test]
4270 fn test_log_roundtrip() {
4271 let old_oid = ObjectId::from_bytes(&[0; 20]).unwrap();
4272 let new_oid = ObjectId::from_bytes(&[0xaa; 20]).unwrap();
4273
4274 let opts = WriteOptions {
4275 write_log: true,
4276 ..Default::default()
4277 };
4278 let mut writer = ReftableWriter::new(opts, 1, 1);
4279 writer
4280 .add_log(LogRecord {
4281 refname: "refs/heads/main".to_owned(),
4282 update_index: 1,
4283 old_id: old_oid,
4284 new_id: new_oid,
4285 name: "Test User".to_owned(),
4286 email: "test@example.com".to_owned(),
4287 time_seconds: 1700000000,
4288 tz_offset: -800,
4289 message: "initial commit".to_owned(),
4290 })
4291 .unwrap();
4292 let data = writer.finish().unwrap();
4293
4294 let reader = ReftableReader::new(data).unwrap();
4295 let logs = reader.read_logs().unwrap();
4296 assert_eq!(logs.len(), 1);
4297 assert_eq!(logs[0].refname, "refs/heads/main");
4298 assert_eq!(logs[0].old_id, old_oid);
4299 assert_eq!(logs[0].new_id, new_oid);
4300 assert_eq!(logs[0].name, "Test User");
4301 assert_eq!(logs[0].email, "test@example.com");
4302 assert_eq!(logs[0].time_seconds, 1700000000);
4303 assert_eq!(logs[0].tz_offset, -800);
4304 assert_eq!(logs[0].message, "initial commit\n");
4308 }
4309
4310 #[test]
4311 fn test_unaligned_table() {
4312 let oid = ObjectId::from_bytes(&[0xcc; 20]).unwrap();
4313 let opts = WriteOptions {
4314 unpadded: true,
4319 restart_interval: 16,
4320 write_log: false,
4321 ..WriteOptions::default()
4322 };
4323 let mut writer = ReftableWriter::new(opts, 1, 1);
4324 writer
4325 .add_ref(RefRecord {
4326 name: "refs/heads/main".to_owned(),
4327 update_index: 1,
4328 value: RefValue::Val1(oid),
4329 })
4330 .unwrap();
4331 let data = writer.finish().unwrap();
4332
4333 assert!(data.len() < DEFAULT_BLOCK_SIZE as usize);
4335
4336 let reader = ReftableReader::new(data).unwrap();
4337 let refs = reader.read_refs().unwrap();
4338 assert_eq!(refs.len(), 1);
4339 assert_eq!(refs[0].value, RefValue::Val1(oid));
4340 }
4341
4342 #[test]
4343 fn test_parse_identity() {
4344 let (name, email, ts, tz) =
4345 parse_identity_string("Test User <test@example.com> 1700000000 -0800");
4346 assert_eq!(name, "Test User");
4347 assert_eq!(email, "test@example.com");
4348 assert_eq!(ts, 1700000000);
4349 assert_eq!(tz, -800);
4351 let (_, _, _, tz) = parse_identity_string("Test User <test@example.com> 1700000000 +0530");
4352 assert_eq!(tz, 530);
4353 }
4354
4355 #[test]
4356 fn test_deletion_record() {
4357 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
4358 writer
4359 .add_ref(RefRecord {
4360 name: "refs/heads/gone".to_owned(),
4361 update_index: 1,
4362 value: RefValue::Deletion,
4363 })
4364 .unwrap();
4365 let data = writer.finish().unwrap();
4366
4367 let reader = ReftableReader::new(data).unwrap();
4368 let refs = reader.read_refs().unwrap();
4369 assert_eq!(refs.len(), 1);
4370 assert_eq!(refs[0].value, RefValue::Deletion);
4371 }
4372
4373 #[test]
4374 fn verify_batch_reftable_symref_ancestor_blocks_child_create() {
4375 use crate::objects::ObjectId;
4376 use crate::refs::{verify_ref_transaction_batch, RefBatchItem, RefnameUnavailable};
4377 use crate::repo::init_repository;
4378
4379 let dir = tempfile::tempdir().expect("tempdir");
4380 init_repository(
4381 dir.path(),
4382 false,
4383 "main",
4384 None,
4385 crate::RefStorageFormat::Reftable,
4386 )
4387 .expect("init");
4388 let git_dir = dir.path().join(".git");
4389 reftable_write_symref(
4390 &git_dir,
4391 "refs/heads/parent",
4392 "refs/heads/missing-target",
4393 None,
4394 None,
4395 )
4396 .expect("symref parent");
4397
4398 let child_oid = ObjectId::from_bytes(&[0x44; 20]).expect("oid");
4399 let items = vec![RefBatchItem {
4400 name: "refs/heads/parent/child".to_owned(),
4401 new_oid: Some(child_oid),
4402 }];
4403 let err = verify_ref_transaction_batch(&git_dir, &items).unwrap_err();
4404 assert!(
4405 matches!(
4406 err,
4407 RefnameUnavailable::AncestorExists {
4408 ref blocking,
4409 ref new_ref,
4410 } if blocking == "refs/heads/parent" && new_ref == "refs/heads/parent/child"
4411 ),
4412 "unexpected {err:?}"
4413 );
4414 }
4415
4416 #[test]
4417 fn update_refs_reftable_symref_ancestor_blocks_child_create() {
4418 use crate::error::Error;
4419 use crate::objects::ObjectId;
4420 use crate::refs::gc::{update_refs, RefTransactionItem};
4421 use crate::refs::store::RefStoreError;
4422 use crate::refs::RefnameUnavailable;
4423 use crate::repo::init_repository;
4424
4425 let dir = tempfile::tempdir().expect("tempdir");
4426 init_repository(
4427 dir.path(),
4428 false,
4429 "main",
4430 None,
4431 crate::RefStorageFormat::Reftable,
4432 )
4433 .expect("init");
4434 let git_dir = dir.path().join(".git");
4435 reftable_write_symref(
4436 &git_dir,
4437 "refs/heads/parent",
4438 "refs/heads/missing-target",
4439 None,
4440 None,
4441 )
4442 .expect("symref parent");
4443
4444 let child_oid = ObjectId::from_bytes(&[0x55; 20]).expect("oid");
4445 let err = update_refs(
4446 &git_dir,
4447 &[RefTransactionItem {
4448 name: "refs/heads/parent/child".to_owned(),
4449 new_oid: Some(child_oid),
4450 expected_old: None,
4451 }],
4452 )
4453 .unwrap_err();
4454 match err {
4455 Error::RefStore(RefStoreError::NameUnavailable {
4456 reason: RefnameUnavailable::AncestorExists { blocking, new_ref },
4457 }) => {
4458 assert_eq!(blocking, "refs/heads/parent");
4459 assert_eq!(new_ref, "refs/heads/parent/child");
4460 }
4461 other => panic!("unexpected {other:?}"),
4462 }
4463 }
4464
4465 #[test]
4466 fn write_transaction_inject_failure_leaves_tables_list_unchanged() {
4467 use crate::objects::ObjectId;
4468 use crate::repo::init_repository;
4469 use tempfile::TempDir;
4470
4471 let tmp = TempDir::new().unwrap();
4472 let repo = init_repository(
4473 tmp.path(),
4474 false,
4475 "main",
4476 None,
4477 crate::RefStorageFormat::Reftable,
4478 )
4479 .unwrap();
4480 let git_dir = repo.git_dir();
4481 let tables_list = git_dir.join("reftable/tables.list");
4482 let before = fs::read_to_string(&tables_list).unwrap_or_default();
4483
4484 let oid = ObjectId::from_bytes(&[0xcd; 20]).unwrap();
4485 set_test_inject_reftable_transaction_fail(true);
4486 let mut stack = ReftableStack::open(&git_dir).unwrap();
4487 let opts = read_write_options(&git_dir);
4488 let err = stack.write_transaction(
4489 vec![ReftableTransactionUpdate {
4490 refname: "refs/heads/inject-fail".to_owned(),
4491 value: Some(RefValue::Val1(oid)),
4492 log: None,
4493 expected_old: None,
4494 }],
4495 &opts,
4496 );
4497 set_test_inject_reftable_transaction_fail(false);
4498 assert!(err.is_err());
4499
4500 let after = fs::read_to_string(&tables_list).unwrap_or_default();
4501 assert_eq!(after, before);
4502 }
4503
4504 #[test]
4505 fn truncated_varint_is_typed() {
4506 let err = get_varint(&[0x80], 0).expect_err("continuation byte without a successor");
4507 assert!(matches!(
4508 err,
4509 Error::Reftable(ReftableError::Truncated {
4510 field: ReftableField::Varint
4511 })
4512 ));
4513 assert!(matches!(
4514 get_varint(&[], 0),
4515 Err(Error::Reftable(ReftableError::Truncated {
4516 field: ReftableField::Varint
4517 }))
4518 ));
4519 }
4520
4521 #[test]
4522 fn writer_rejects_unsorted_refs_with_both_names() {
4523 let oid = ObjectId::from_bytes(&[0x11; 20]).expect("oid");
4524 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
4525 writer
4526 .add_ref(RefRecord {
4527 name: "refs/heads/b".to_owned(),
4528 update_index: 1,
4529 value: RefValue::Val1(oid),
4530 })
4531 .expect("first ref");
4532 let err = writer
4533 .add_ref(RefRecord {
4534 name: "refs/heads/a".to_owned(),
4535 update_index: 1,
4536 value: RefValue::Val1(oid),
4537 })
4538 .expect_err("out of order");
4539 match err {
4540 Error::Reftable(ReftableError::UnsortedRefs { previous, next }) => {
4541 assert_eq!(previous, "refs/heads/b");
4542 assert_eq!(next, "refs/heads/a");
4543 }
4544 other => panic!("unexpected {other:?}"),
4545 }
4546 }
4547
4548 #[test]
4549 fn reader_reports_footer_version_mismatch_and_short_files() {
4550 let data = ReftableWriter::new(WriteOptions::default(), 1, 1)
4551 .finish()
4552 .expect("empty table");
4553 let footer_start = data.len() - FOOTER_V1_SIZE;
4554
4555 let mut mismatched = data.clone();
4556 mismatched[footer_start + 4] = 2;
4557 assert!(matches!(
4558 ReftableReader::new(mismatched),
4559 Err(Error::Reftable(ReftableError::FooterVersionMismatch {
4560 header: 1,
4561 footer: 2
4562 }))
4563 ));
4564
4565 assert!(matches!(
4566 ReftableReader::new(data[..10].to_vec()),
4567 Err(Error::Reftable(ReftableError::TooSmall { len: 10 }))
4568 ));
4569 assert!(matches!(
4570 ReftableReader::new(data[..HEADER_SIZE + 4].to_vec()),
4571 Err(Error::Reftable(ReftableError::TooSmall { .. }))
4572 ));
4573 }
4574
4575 #[test]
4576 fn write_transaction_reports_expected_mismatch() {
4577 use crate::repo::init_repository;
4578
4579 let tmp = tempfile::tempdir().expect("tempdir");
4580 let repo = init_repository(
4581 tmp.path(),
4582 false,
4583 "main",
4584 None,
4585 crate::RefStorageFormat::Reftable,
4586 )
4587 .expect("init");
4588 let git_dir = repo.git_dir();
4589 let opts = read_write_options(&git_dir);
4590 let current = ObjectId::from_bytes(&[0x21; 20]).expect("oid");
4591 let stale = ObjectId::from_bytes(&[0x22; 20]).expect("oid");
4592 let mut stack = ReftableStack::open(&git_dir).expect("open");
4593 stack
4594 .write_transaction(
4595 vec![ReftableTransactionUpdate {
4596 refname: "refs/heads/topic".to_owned(),
4597 value: Some(RefValue::Val1(current)),
4598 log: None,
4599 expected_old: None,
4600 }],
4601 &opts,
4602 )
4603 .expect("seed topic");
4604
4605 let update = |refname: &str| ReftableTransactionUpdate {
4606 refname: refname.to_owned(),
4607 value: Some(RefValue::Val1(stale)),
4608 log: None,
4609 expected_old: Some(stale),
4610 };
4611 match stack.write_transaction(vec![update("refs/heads/topic")], &opts) {
4612 Err(Error::Reftable(ReftableError::ExpectedMismatch {
4613 refname,
4614 expected,
4615 actual,
4616 })) => {
4617 assert_eq!(refname, "refs/heads/topic");
4618 assert_eq!(expected, stale);
4619 assert_eq!(actual, Some(current));
4620 }
4621 other => panic!("unexpected {other:?}"),
4622 }
4623 let err = stack
4624 .write_transaction(vec![update("refs/heads/absent")], &opts)
4625 .expect_err("absent ref");
4626 assert!(matches!(
4627 &err,
4628 Error::Reftable(ReftableError::ExpectedMismatch { actual: None, .. })
4629 ));
4630 assert!(err.to_string().ends_with("but found <absent>"), "{err}");
4631 }
4632
4633 #[test]
4634 fn error_display_names_the_field() {
4635 assert_eq!(
4636 ReftableError::Truncated {
4637 field: ReftableField::LogEmail
4638 }
4639 .to_string(),
4640 "reftable: truncated log email"
4641 );
4642 assert_eq!(
4643 Error::from(ReftableError::InvalidUtf8 {
4644 field: ReftableField::SymrefTarget
4645 })
4646 .to_string(),
4647 "reftable: symref target is not valid UTF-8"
4648 );
4649 }
4650}