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