1use std::collections::BTreeMap;
29use std::fs;
30use std::io::{Read, Write};
31use std::path::{Path, PathBuf};
32
33use crate::config::ConfigSet;
34use crate::error::{Error, Result};
35use crate::objects::ObjectId;
36
37const REFTABLE_MAGIC: &[u8; 4] = b"REFT";
43
44const HEADER_SIZE: usize = 24;
47
48const FOOTER_V1_SIZE: usize = 68;
50
51const BLOCK_TYPE_REF: u8 = b'r';
53const BLOCK_TYPE_INDEX: u8 = b'i';
55const BLOCK_TYPE_LOG: u8 = b'g';
57
58const VALUE_DELETION: u8 = 0;
60const VALUE_ONE_OID: u8 = 1;
61const VALUE_TWO_OID: u8 = 2;
62const VALUE_SYMREF: u8 = 3;
63
64const HASH_SIZE: usize = 20;
66
67const DEFAULT_BLOCK_SIZE: u32 = 4096;
69
70const RESTART_INTERVAL: usize = 16;
72
73fn put_varint(mut val: u64, out: &mut Vec<u8>) -> usize {
79 let mut buf = [0u8; 10];
81 let mut i = 0;
82 buf[i] = (val & 0x7f) as u8;
83 i += 1;
84 val >>= 7;
85 while val > 0 {
86 val -= 1;
87 buf[i] = (val & 0x7f) as u8;
88 i += 1;
89 val >>= 7;
90 }
91 let len = i;
93 for j in (1..len).rev() {
94 out.push(buf[j] | 0x80);
95 }
96 out.push(buf[0]);
97 len
98}
99
100fn get_varint(data: &[u8], mut pos: usize) -> Result<(u64, usize)> {
102 if pos >= data.len() {
103 return Err(Error::InvalidRef("varint: unexpected end of data".into()));
104 }
105 let mut val = (data[pos] & 0x7f) as u64;
106 while data[pos] & 0x80 != 0 {
107 pos += 1;
108 if pos >= data.len() {
109 return Err(Error::InvalidRef("varint: unexpected end of data".into()));
110 }
111 val = ((val + 1) << 7) | (data[pos] & 0x7f) as u64;
112 }
113 Ok((val, pos + 1))
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
122pub enum RefValue {
123 Deletion,
125 Val1(ObjectId),
127 Val2(ObjectId, ObjectId),
129 Symref(String),
131}
132
133#[derive(Debug, Clone)]
135pub struct RefRecord {
136 pub name: String,
138 pub update_index: u64,
140 pub value: RefValue,
142}
143
144#[derive(Debug, Clone)]
146pub struct LogRecord {
147 pub refname: String,
149 pub update_index: u64,
151 pub old_id: ObjectId,
153 pub new_id: ObjectId,
155 pub name: String,
157 pub email: String,
159 pub time_seconds: u64,
161 pub tz_offset: i16,
163 pub message: String,
165}
166
167#[derive(Debug, Clone)]
169pub struct WriteOptions {
170 pub block_size: u32,
172 pub restart_interval: usize,
174 pub write_log: bool,
176}
177
178impl Default for WriteOptions {
179 fn default() -> Self {
180 Self {
181 block_size: DEFAULT_BLOCK_SIZE,
182 restart_interval: RESTART_INTERVAL,
183 write_log: true,
184 }
185 }
186}
187
188pub struct ReftableWriter {
202 opts: WriteOptions,
203 min_update_index: u64,
204 max_update_index: u64,
205
206 refs: Vec<RefRecord>,
208 logs: Vec<LogRecord>,
210}
211
212impl ReftableWriter {
213 pub fn new(opts: WriteOptions, min_update_index: u64, max_update_index: u64) -> Self {
215 Self {
216 opts,
217 min_update_index,
218 max_update_index,
219 refs: Vec::new(),
220 logs: Vec::new(),
221 }
222 }
223
224 pub fn add_ref(&mut self, rec: RefRecord) -> Result<()> {
226 if let Some(last) = self.refs.last() {
227 if rec.name <= last.name {
228 return Err(Error::InvalidRef(format!(
229 "reftable: refs must be sorted, got '{}' after '{}'",
230 rec.name, last.name
231 )));
232 }
233 }
234 self.refs.push(rec);
235 Ok(())
236 }
237
238 pub fn add_log(&mut self, rec: LogRecord) -> Result<()> {
240 self.logs.push(rec);
241 Ok(())
242 }
243
244 pub fn finish(mut self) -> Result<Vec<u8>> {
246 let mut out = Vec::new();
247 let block_size = self.opts.block_size;
248
249 out.extend_from_slice(REFTABLE_MAGIC);
251 out.push(1); out.push(((block_size >> 16) & 0xff) as u8);
253 out.push(((block_size >> 8) & 0xff) as u8);
254 out.push((block_size & 0xff) as u8);
255 out.extend_from_slice(&self.min_update_index.to_be_bytes());
256 out.extend_from_slice(&self.max_update_index.to_be_bytes());
257
258 assert_eq!(out.len(), HEADER_SIZE);
259
260 let ref_block_positions = self.write_ref_blocks(&mut out)?;
262
263 let ref_index_position = if ref_block_positions.len() >= 4 {
265 let pos = out.len() as u64;
266 self.write_ref_index(&mut out, &ref_block_positions)?;
267 pos
268 } else {
269 0
270 };
271
272 let log_position = if self.opts.write_log && !self.logs.is_empty() {
274 let pos = out.len() as u64;
275 self.write_log_blocks(&mut out)?;
276 pos
277 } else {
278 0
279 };
280
281 let footer_start = out.len();
283 out.extend_from_slice(REFTABLE_MAGIC);
285 out.push(1);
286 out.push(((block_size >> 16) & 0xff) as u8);
287 out.push(((block_size >> 8) & 0xff) as u8);
288 out.push((block_size & 0xff) as u8);
289 out.extend_from_slice(&self.min_update_index.to_be_bytes());
290 out.extend_from_slice(&self.max_update_index.to_be_bytes());
291
292 out.extend_from_slice(&ref_index_position.to_be_bytes());
294 out.extend_from_slice(&0u64.to_be_bytes());
296 out.extend_from_slice(&0u64.to_be_bytes());
298 out.extend_from_slice(&log_position.to_be_bytes());
300 out.extend_from_slice(&0u64.to_be_bytes());
302
303 let crc = crc32(&out[footer_start..]);
305 out.extend_from_slice(&crc.to_be_bytes());
306
307 Ok(out)
308 }
309
310 fn write_ref_blocks(&self, out: &mut Vec<u8>) -> Result<Vec<(u64, String)>> {
312 if self.refs.is_empty() {
313 return Ok(Vec::new());
314 }
315
316 let block_size = self.opts.block_size as usize;
317 let restart_interval = self.opts.restart_interval;
318 let mut block_positions: Vec<(u64, String)> = Vec::new();
319 let mut i = 0;
320
321 while i < self.refs.len() {
322 let block_start = out.len();
323 let is_first_block = block_start == HEADER_SIZE;
324
325 let mut records_buf = Vec::new();
327 let mut restart_offsets: Vec<u32> = Vec::new();
328 let mut prev_name = String::new();
329 let mut count = 0;
330 let mut last_name = String::new();
331
332 while i < self.refs.len() {
333 let rec = &self.refs[i];
334 let is_restart = count % restart_interval == 0;
335
336 let mut rec_buf = Vec::new();
337 let prefix_len = if is_restart {
338 0
339 } else {
340 common_prefix_len(prev_name.as_bytes(), rec.name.as_bytes())
341 };
342 let suffix = &rec.name.as_bytes()[prefix_len..];
343 let suffix_len = suffix.len();
344
345 let value_type = match &rec.value {
346 RefValue::Deletion => VALUE_DELETION,
347 RefValue::Val1(_) => VALUE_ONE_OID,
348 RefValue::Val2(_, _) => VALUE_TWO_OID,
349 RefValue::Symref(_) => VALUE_SYMREF,
350 };
351
352 put_varint(prefix_len as u64, &mut rec_buf);
353 put_varint(((suffix_len as u64) << 3) | value_type as u64, &mut rec_buf);
354 rec_buf.extend_from_slice(suffix);
355
356 let update_index_delta = rec.update_index.saturating_sub(self.min_update_index);
357 put_varint(update_index_delta, &mut rec_buf);
358
359 match &rec.value {
360 RefValue::Deletion => {}
361 RefValue::Val1(oid) => {
362 rec_buf.extend_from_slice(oid.as_bytes());
363 }
364 RefValue::Val2(oid, peeled) => {
365 rec_buf.extend_from_slice(oid.as_bytes());
366 rec_buf.extend_from_slice(peeled.as_bytes());
367 }
368 RefValue::Symref(target) => {
369 put_varint(target.len() as u64, &mut rec_buf);
370 rec_buf.extend_from_slice(target.as_bytes());
371 }
372 }
373
374 let restart_count = restart_offsets.len() + if is_restart { 1 } else { 0 };
377 let trailer_size = restart_count * 3 + 2;
378 let total = 4 + records_buf.len() + rec_buf.len() + trailer_size;
379 let effective_block_size = if is_first_block && block_size > 0 {
380 block_size } else if block_size > 0 {
382 block_size
383 } else {
384 usize::MAX };
386 let block_len = if is_first_block {
388 HEADER_SIZE + total
389 } else {
390 total
391 };
392
393 if block_size > 0 && block_len > effective_block_size && count > 0 {
394 break; }
396
397 if is_restart {
398 let offset = if is_first_block {
399 HEADER_SIZE + 4 + records_buf.len()
400 } else {
401 4 + records_buf.len()
402 };
403 restart_offsets.push(offset as u32);
404 }
405
406 records_buf.extend_from_slice(&rec_buf);
407 last_name = rec.name.clone();
408 prev_name = rec.name.clone();
409 count += 1;
410 i += 1;
411 }
412
413 if count == 0 {
414 return Err(Error::InvalidRef(
415 "reftable: ref record too large for block size".into(),
416 ));
417 }
418
419 if restart_offsets.is_empty() {
421 restart_offsets.push(if is_first_block {
422 HEADER_SIZE as u32 + 4
423 } else {
424 4
425 });
426 }
427
428 let trailer_size = restart_offsets.len() * 3 + 2;
430 let block_len_val = if is_first_block {
431 HEADER_SIZE + 4 + records_buf.len() + trailer_size
432 } else {
433 4 + records_buf.len() + trailer_size
434 };
435
436 out.push(BLOCK_TYPE_REF);
438 out.push(((block_len_val >> 16) & 0xff) as u8);
439 out.push(((block_len_val >> 8) & 0xff) as u8);
440 out.push((block_len_val & 0xff) as u8);
441
442 out.extend_from_slice(&records_buf);
444
445 for &off in &restart_offsets {
447 out.push(((off >> 16) & 0xff) as u8);
448 out.push(((off >> 8) & 0xff) as u8);
449 out.push((off & 0xff) as u8);
450 }
451
452 let rc = restart_offsets.len() as u16;
454 out.push((rc >> 8) as u8);
455 out.push((rc & 0xff) as u8);
456
457 if block_size > 0 {
459 let written = out.len() - block_start;
460 let target = if is_first_block {
461 block_size
462 } else {
463 block_size
464 };
465 if written < target {
466 out.resize(block_start + target, 0);
467 }
468 }
469
470 block_positions.push((block_start as u64, last_name.clone()));
471 }
472
473 Ok(block_positions)
474 }
475
476 fn write_ref_index(&self, out: &mut Vec<u8>, block_positions: &[(u64, String)]) -> Result<()> {
478 let mut records_buf = Vec::new();
479 let mut restart_offsets: Vec<u32> = Vec::new();
480 let mut prev_name = String::new();
481
482 for (idx, (block_pos, last_ref)) in block_positions.iter().enumerate() {
483 let is_restart = idx % self.opts.restart_interval == 0;
484 let prefix_len = if is_restart {
485 0
486 } else {
487 common_prefix_len(prev_name.as_bytes(), last_ref.as_bytes())
488 };
489 let suffix = &last_ref.as_bytes()[prefix_len..];
490
491 if is_restart {
492 restart_offsets.push(4 + records_buf.len() as u32);
493 }
494
495 put_varint(prefix_len as u64, &mut records_buf);
496 put_varint((suffix.len() as u64) << 3, &mut records_buf);
497 records_buf.extend_from_slice(suffix);
498 put_varint(*block_pos, &mut records_buf);
499
500 prev_name = last_ref.clone();
501 }
502
503 if restart_offsets.is_empty() {
504 restart_offsets.push(4);
505 }
506
507 let trailer_size = restart_offsets.len() * 3 + 2;
508 let block_len = 4 + records_buf.len() + trailer_size;
509
510 out.push(BLOCK_TYPE_INDEX);
511 out.push(((block_len >> 16) & 0xff) as u8);
512 out.push(((block_len >> 8) & 0xff) as u8);
513 out.push((block_len & 0xff) as u8);
514
515 out.extend_from_slice(&records_buf);
516
517 for &off in &restart_offsets {
518 out.push(((off >> 16) & 0xff) as u8);
519 out.push(((off >> 8) & 0xff) as u8);
520 out.push((off & 0xff) as u8);
521 }
522 let rc = restart_offsets.len() as u16;
523 out.push((rc >> 8) as u8);
524 out.push((rc & 0xff) as u8);
525
526 Ok(())
527 }
528
529 fn write_log_blocks(&mut self, out: &mut Vec<u8>) -> Result<()> {
531 use flate2::write::DeflateEncoder;
532 use flate2::Compression;
533
534 self.logs.sort_by(|a, b| {
536 a.refname
537 .cmp(&b.refname)
538 .then_with(|| b.update_index.cmp(&a.update_index))
539 });
540
541 let mut inner = Vec::new();
543 let mut restart_offsets: Vec<u32> = Vec::new();
544 let mut prev_key = Vec::<u8>::new();
545
546 for (idx, log) in self.logs.iter().enumerate() {
547 let is_restart = idx % self.opts.restart_interval == 0;
548
549 let mut key = Vec::new();
551 key.extend_from_slice(log.refname.as_bytes());
552 key.push(0);
553 key.extend_from_slice(&(0xffffffffffffffffu64 - log.update_index).to_be_bytes());
554
555 let prefix_len = if is_restart {
556 0
557 } else {
558 common_prefix_len(&prev_key, &key)
559 };
560 let suffix = &key[prefix_len..];
561
562 if is_restart {
563 restart_offsets.push(4 + inner.len() as u32);
565 }
566
567 let log_type: u8 = 1;
569 put_varint(prefix_len as u64, &mut inner);
570 put_varint(((suffix.len() as u64) << 3) | log_type as u64, &mut inner);
571 inner.extend_from_slice(suffix);
572
573 inner.extend_from_slice(log.old_id.as_bytes());
575 inner.extend_from_slice(log.new_id.as_bytes());
576 put_varint(log.name.len() as u64, &mut inner);
577 inner.extend_from_slice(log.name.as_bytes());
578 put_varint(log.email.len() as u64, &mut inner);
579 inner.extend_from_slice(log.email.as_bytes());
580 put_varint(log.time_seconds, &mut inner);
581 inner.extend_from_slice(&log.tz_offset.to_be_bytes());
582 put_varint(log.message.len() as u64, &mut inner);
583 inner.extend_from_slice(log.message.as_bytes());
584
585 prev_key = key;
586 }
587
588 if restart_offsets.is_empty() {
589 restart_offsets.push(4);
590 }
591
592 for &off in &restart_offsets {
594 inner.push(((off >> 16) & 0xff) as u8);
595 inner.push(((off >> 8) & 0xff) as u8);
596 inner.push((off & 0xff) as u8);
597 }
598 let rc = restart_offsets.len() as u16;
599 inner.push((rc >> 8) as u8);
600 inner.push((rc & 0xff) as u8);
601
602 let block_len = 4 + inner.len();
604
605 let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
607 encoder
608 .write_all(&inner)
609 .map_err(|e| Error::Zlib(e.to_string()))?;
610 let compressed = encoder.finish().map_err(|e| Error::Zlib(e.to_string()))?;
611
612 out.push(BLOCK_TYPE_LOG);
614 out.push(((block_len >> 16) & 0xff) as u8);
615 out.push(((block_len >> 8) & 0xff) as u8);
616 out.push((block_len & 0xff) as u8);
617 out.extend_from_slice(&compressed);
618
619 Ok(())
620 }
621}
622
623pub struct ReftableReader {
629 data: Vec<u8>,
630 version: u8,
631 block_size: u32,
632 min_update_index: u64,
633 max_update_index: u64,
634 ref_index_position: u64,
635 log_position: u64,
636}
637
638#[derive(Debug)]
640#[allow(dead_code)]
641struct Footer {
642 version: u8,
643 block_size: u32,
644 min_update_index: u64,
645 max_update_index: u64,
646 ref_index_position: u64,
647 obj_position_and_id_len: u64,
648 obj_index_position: u64,
649 log_position: u64,
650 log_index_position: u64,
651}
652
653impl ReftableReader {
654 pub fn new(data: Vec<u8>) -> Result<Self> {
656 if data.len() < HEADER_SIZE + FOOTER_V1_SIZE {
657 if data.len() < HEADER_SIZE {
659 return Err(Error::InvalidRef("reftable: file too small".into()));
660 }
661 }
662
663 if &data[0..4] != REFTABLE_MAGIC {
665 return Err(Error::InvalidRef("reftable: bad magic".into()));
666 }
667 let version = data[4];
668 if version != 1 && version != 2 {
669 return Err(Error::InvalidRef(format!(
670 "reftable: unsupported version {version}"
671 )));
672 }
673 let _block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
674 let _min_update_index = u64::from_be_bytes(data[8..16].try_into().unwrap());
675 let _max_update_index = u64::from_be_bytes(data[16..24].try_into().unwrap());
676
677 let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
679 if data.len() < footer_size {
680 return Err(Error::InvalidRef(
681 "reftable: file too small for footer".into(),
682 ));
683 }
684 let footer_start = data.len() - footer_size;
685 let footer = parse_footer(&data[footer_start..], version)?;
686
687 Ok(Self {
688 data,
689 version,
690 block_size: footer.block_size,
691 min_update_index: footer.min_update_index,
692 max_update_index: footer.max_update_index,
693 ref_index_position: footer.ref_index_position,
694 log_position: footer.log_position,
695 })
696 }
697
698 pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
700 let mut refs = Vec::new();
701 let footer_size = if self.version == 2 {
702 72
703 } else {
704 FOOTER_V1_SIZE
705 };
706 let file_end = self.data.len() - footer_size;
707
708 let ref_end = if self.ref_index_position > 0 {
710 self.ref_index_position as usize
711 } else if self.log_position > 0 {
712 self.log_position as usize
713 } else {
714 file_end
715 };
716
717 let mut pos = 0usize;
718 if pos < HEADER_SIZE {
721 pos = HEADER_SIZE;
722 }
723
724 while pos < ref_end {
725 if pos >= self.data.len() {
726 break;
727 }
728 let block_type = self.data[pos];
729 if block_type == 0 {
730 if self.block_size > 0 {
732 let bs = self.block_size as usize;
733 pos = ((pos / bs) + 1) * bs;
734 continue;
735 } else {
736 break;
737 }
738 }
739 if block_type != BLOCK_TYPE_REF {
740 break;
741 }
742
743 let block_len = read_u24(&self.data, pos + 1);
744 let block_data_start = pos + 4; let is_first = pos == HEADER_SIZE;
749 let records_end = if is_first {
750 block_len
752 } else {
753 pos + block_len
754 };
755
756 if records_end > ref_end {
757 break;
758 }
759
760 let rc = read_u16(&self.data, records_end - 2);
762 let restart_table_start = records_end - 2 - (rc * 3);
764
765 let mut rpos = block_data_start;
767 let mut prev_name = Vec::<u8>::new();
768
769 while rpos < restart_table_start {
770 let (rec, new_pos) =
771 decode_ref_record(&self.data, rpos, &prev_name, self.min_update_index)?;
772 prev_name = rec.name.as_bytes().to_vec();
773 refs.push(rec);
774 rpos = new_pos;
775 }
776
777 if self.block_size > 0 {
779 let bs = self.block_size as usize;
780 if is_first {
781 pos = bs;
782 } else {
783 pos += bs;
784 }
785 } else {
786 pos = records_end;
787 }
788 }
789
790 Ok(refs)
791 }
792
793 pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
795 let refs = self.read_refs()?;
797 Ok(refs.into_iter().find(|r| r.name == name))
798 }
799
800 pub fn read_logs(&self) -> Result<Vec<LogRecord>> {
802 if self.log_position == 0 {
803 return Ok(Vec::new());
804 }
805
806 let footer_size = if self.version == 2 {
807 72
808 } else {
809 FOOTER_V1_SIZE
810 };
811 let file_end = self.data.len() - footer_size;
812 let mut pos = self.log_position as usize;
813 let mut logs = Vec::new();
814
815 while pos < file_end {
816 if pos >= self.data.len() {
817 break;
818 }
819 let block_type = self.data[pos];
820 if block_type != BLOCK_TYPE_LOG {
821 break;
822 }
823 let block_len = read_u24(&self.data, pos + 1);
824 let compressed_start = pos + 4;
825
826 let inflated_size = block_len - 4;
828
829 use flate2::read::DeflateDecoder;
831 let remaining = &self.data[compressed_start..file_end];
832 let mut decoder = DeflateDecoder::new(remaining);
833 let mut inflated = vec![0u8; inflated_size];
834 decoder
835 .read_exact(&mut inflated)
836 .map_err(|e| Error::Zlib(e.to_string()))?;
837
838 let consumed = decoder.total_in() as usize;
840
841 if inflated.len() < 2 {
844 break;
845 }
846 let rc = read_u16(&inflated, inflated.len() - 2);
847 let restart_table_start = inflated.len() - 2 - (rc * 3);
848
849 let mut rpos = 0usize;
850 let mut prev_key = Vec::<u8>::new();
851
852 while rpos < restart_table_start {
853 let (log, new_pos) = decode_log_record(&inflated, rpos, &prev_key)?;
854 let mut key = Vec::new();
856 key.extend_from_slice(log.refname.as_bytes());
857 key.push(0);
858 key.extend_from_slice(&(0xffffffffffffffffu64 - log.update_index).to_be_bytes());
859 prev_key = key;
860 logs.push(log);
861 rpos = new_pos;
862 }
863
864 pos = compressed_start + consumed;
865 }
866
867 Ok(logs)
868 }
869
870 pub fn block_size(&self) -> u32 {
872 self.block_size
873 }
874
875 pub fn min_update_index(&self) -> u64 {
877 self.min_update_index
878 }
879
880 pub fn max_update_index(&self) -> u64 {
882 self.max_update_index
883 }
884}
885
886fn decode_ref_record(
891 data: &[u8],
892 pos: usize,
893 prev_name: &[u8],
894 min_update_index: u64,
895) -> Result<(RefRecord, usize)> {
896 let (prefix_len, p) = get_varint(data, pos)?;
897 let (suffix_and_type, mut p) = get_varint(data, p)?;
898 let suffix_len = (suffix_and_type >> 3) as usize;
899 let value_type = (suffix_and_type & 0x7) as u8;
900
901 let mut name = Vec::with_capacity(prefix_len as usize + suffix_len);
903 if prefix_len > 0 {
904 if (prefix_len as usize) > prev_name.len() {
905 return Err(Error::InvalidRef(
906 "reftable: prefix_len exceeds prev name".into(),
907 ));
908 }
909 name.extend_from_slice(&prev_name[..prefix_len as usize]);
910 }
911 if p + suffix_len > data.len() {
912 return Err(Error::InvalidRef("reftable: suffix overflows block".into()));
913 }
914 name.extend_from_slice(&data[p..p + suffix_len]);
915 p += suffix_len;
916
917 let name_str = String::from_utf8(name)
918 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in ref name".into()))?;
919
920 let (update_index_delta, mut p) = get_varint(data, p)?;
921 let update_index = min_update_index + update_index_delta;
922
923 let value = match value_type {
924 VALUE_DELETION => RefValue::Deletion,
925 VALUE_ONE_OID => {
926 if p + HASH_SIZE > data.len() {
927 return Err(Error::InvalidRef("reftable: truncated OID".into()));
928 }
929 let oid = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
930 p += HASH_SIZE;
931 RefValue::Val1(oid)
932 }
933 VALUE_TWO_OID => {
934 if p + 2 * HASH_SIZE > data.len() {
935 return Err(Error::InvalidRef("reftable: truncated OID pair".into()));
936 }
937 let oid = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
938 p += HASH_SIZE;
939 let peeled = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
940 p += HASH_SIZE;
941 RefValue::Val2(oid, peeled)
942 }
943 VALUE_SYMREF => {
944 let (target_len, p2) = get_varint(data, p)?;
945 p = p2;
946 let target_len = target_len as usize;
947 if p + target_len > data.len() {
948 return Err(Error::InvalidRef(
949 "reftable: truncated symref target".into(),
950 ));
951 }
952 let target = String::from_utf8(data[p..p + target_len].to_vec())
953 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in symref".into()))?;
954 p += target_len;
955 RefValue::Symref(target)
956 }
957 _ => {
958 return Err(Error::InvalidRef(format!(
959 "reftable: unknown value_type {value_type}"
960 )));
961 }
962 };
963
964 Ok((
965 RefRecord {
966 name: name_str,
967 update_index,
968 value,
969 },
970 p,
971 ))
972}
973
974fn decode_log_record(data: &[u8], pos: usize, prev_key: &[u8]) -> Result<(LogRecord, usize)> {
975 let (prefix_len, p) = get_varint(data, pos)?;
976 let (suffix_and_type, mut p) = get_varint(data, p)?;
977 let suffix_len = (suffix_and_type >> 3) as usize;
978 let log_type = (suffix_and_type & 0x7) as u8;
979
980 let mut key = Vec::with_capacity(prefix_len as usize + suffix_len);
982 if prefix_len > 0 {
983 if (prefix_len as usize) > prev_key.len() {
984 return Err(Error::InvalidRef(
985 "reftable: log prefix_len exceeds prev key".into(),
986 ));
987 }
988 key.extend_from_slice(&prev_key[..prefix_len as usize]);
989 }
990 if p + suffix_len > data.len() {
991 return Err(Error::InvalidRef("reftable: log suffix overflows".into()));
992 }
993 key.extend_from_slice(&data[p..p + suffix_len]);
994 p += suffix_len;
995
996 let null_pos = key
998 .iter()
999 .position(|&b| b == 0)
1000 .ok_or_else(|| Error::InvalidRef("reftable: log key missing null separator".into()))?;
1001 let refname = String::from_utf8(key[..null_pos].to_vec())
1002 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log refname".into()))?;
1003 if null_pos + 9 > key.len() {
1004 return Err(Error::InvalidRef("reftable: log key too short".into()));
1005 }
1006 let reversed_idx = u64::from_be_bytes(key[null_pos + 1..null_pos + 9].try_into().unwrap());
1007 let update_index = 0xffffffffffffffffu64 - reversed_idx;
1008
1009 if log_type == 0 {
1010 let zero_oid = ObjectId::from_bytes(&[0u8; 20])?;
1012 return Ok((
1013 LogRecord {
1014 refname,
1015 update_index,
1016 old_id: zero_oid,
1017 new_id: zero_oid,
1018 name: String::new(),
1019 email: String::new(),
1020 time_seconds: 0,
1021 tz_offset: 0,
1022 message: String::new(),
1023 },
1024 p,
1025 ));
1026 }
1027
1028 if p + 2 * HASH_SIZE > data.len() {
1030 return Err(Error::InvalidRef("reftable: truncated log OIDs".into()));
1031 }
1032 let old_id = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
1033 p += HASH_SIZE;
1034 let new_id = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
1035 p += HASH_SIZE;
1036
1037 let (name_len, p2) = get_varint(data, p)?;
1038 p = p2;
1039 let name_len = name_len as usize;
1040 if p + name_len > data.len() {
1041 return Err(Error::InvalidRef("reftable: truncated log name".into()));
1042 }
1043 let name = String::from_utf8(data[p..p + name_len].to_vec())
1044 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log name".into()))?;
1045 p += name_len;
1046
1047 let (email_len, p2) = get_varint(data, p)?;
1048 p = p2;
1049 let email_len = email_len as usize;
1050 if p + email_len > data.len() {
1051 return Err(Error::InvalidRef("reftable: truncated log email".into()));
1052 }
1053 let email = String::from_utf8(data[p..p + email_len].to_vec())
1054 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log email".into()))?;
1055 p += email_len;
1056
1057 let (time_seconds, p2) = get_varint(data, p)?;
1058 p = p2;
1059
1060 if p + 2 > data.len() {
1061 return Err(Error::InvalidRef("reftable: truncated tz_offset".into()));
1062 }
1063 let tz_offset = i16::from_be_bytes([data[p], data[p + 1]]);
1064 p += 2;
1065
1066 let (msg_len, p2) = get_varint(data, p)?;
1067 p = p2;
1068 let msg_len = msg_len as usize;
1069 if p + msg_len > data.len() {
1070 return Err(Error::InvalidRef("reftable: truncated log message".into()));
1071 }
1072 let message = String::from_utf8(data[p..p + msg_len].to_vec())
1073 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log message".into()))?;
1074 p += msg_len;
1075
1076 Ok((
1077 LogRecord {
1078 refname,
1079 update_index,
1080 old_id,
1081 new_id,
1082 name,
1083 email,
1084 time_seconds,
1085 tz_offset,
1086 message,
1087 },
1088 p,
1089 ))
1090}
1091
1092pub struct ReftableStack {
1101 reftable_dir: PathBuf,
1103 table_names: Vec<String>,
1105}
1106
1107impl ReftableStack {
1108 pub fn open(git_dir: &Path) -> Result<Self> {
1110 let reftable_dir = git_dir.join("reftable");
1111 let tables_list = reftable_dir.join("tables.list");
1112 let content = fs::read_to_string(&tables_list).map_err(Error::Io)?;
1113 let table_names: Vec<String> = content
1114 .lines()
1115 .filter(|l| !l.is_empty())
1116 .map(|l| l.to_owned())
1117 .collect();
1118 Ok(Self {
1119 reftable_dir,
1120 table_names,
1121 })
1122 }
1123
1124 pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
1129 let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
1130
1131 for name in &self.table_names {
1132 let path = self.reftable_dir.join(name);
1133 let data = fs::read(&path).map_err(Error::Io)?;
1134 let reader = ReftableReader::new(data)?;
1135 for rec in reader.read_refs()? {
1136 match &rec.value {
1137 RefValue::Deletion => {
1138 merged.remove(&rec.name);
1139 }
1140 _ => {
1141 merged.insert(rec.name.clone(), rec);
1142 }
1143 }
1144 }
1145 }
1146
1147 Ok(merged.into_values().collect())
1148 }
1149
1150 pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
1152 for table_name in self.table_names.iter().rev() {
1154 let path = self.reftable_dir.join(table_name);
1155 let data = fs::read(&path).map_err(Error::Io)?;
1156 let reader = ReftableReader::new(data)?;
1157 if let Some(rec) = reader.lookup_ref(name)? {
1158 return match rec.value {
1159 RefValue::Deletion => Ok(None),
1160 _ => Ok(Some(rec)),
1161 };
1162 }
1163 }
1164 Ok(None)
1165 }
1166
1167 pub fn read_logs_for_ref(&self, refname: &str) -> Result<Vec<LogRecord>> {
1169 let mut logs = Vec::new();
1170 for table_name in &self.table_names {
1171 let path = self.reftable_dir.join(table_name);
1172 let data = fs::read(&path).map_err(Error::Io)?;
1173 let reader = ReftableReader::new(data)?;
1174 for log in reader.read_logs()? {
1175 if log.refname == refname {
1176 logs.push(log);
1177 }
1178 }
1179 }
1180 logs.sort_by(|a, b| b.update_index.cmp(&a.update_index));
1182 Ok(logs)
1183 }
1184
1185 pub fn read_all_logs(&self) -> Result<Vec<LogRecord>> {
1187 let mut logs = Vec::new();
1188 for table_name in &self.table_names {
1189 let path = self.reftable_dir.join(table_name);
1190 let data = fs::read(&path).map_err(Error::Io)?;
1191 let reader = ReftableReader::new(data)?;
1192 logs.extend(reader.read_logs()?);
1193 }
1194 logs.sort_by(|a, b| {
1195 a.refname
1196 .cmp(&b.refname)
1197 .then_with(|| b.update_index.cmp(&a.update_index))
1198 });
1199 Ok(logs)
1200 }
1201
1202 pub fn max_update_index(&self) -> Result<u64> {
1204 let mut max_idx = 0u64;
1205 for name in &self.table_names {
1206 let path = self.reftable_dir.join(name);
1207 let data = fs::read(&path).map_err(Error::Io)?;
1208 let reader = ReftableReader::new(data)?;
1209 max_idx = max_idx.max(reader.max_update_index());
1210 }
1211 Ok(max_idx)
1212 }
1213
1214 pub fn add_table(&mut self, data: &[u8], update_index: u64) -> Result<String> {
1219 let random: u64 = {
1220 let mut buf = [0u8; 8];
1222 if let Ok(mut f) = fs::File::open("/dev/urandom") {
1223 let _ = f.read(&mut buf);
1224 }
1225 u64::from_le_bytes(buf)
1226 };
1227 let filename = format!(
1228 "{:08x}-{:08x}-{:08x}.ref",
1229 update_index, update_index, random as u32
1230 );
1231 let path = self.reftable_dir.join(&filename);
1232 fs::write(&path, data).map_err(Error::Io)?;
1233
1234 self.table_names.push(filename.clone());
1235 self.write_tables_list()?;
1236
1237 if self.table_names.len() > 3
1241 && std::env::var("GIT_TEST_REFTABLE_AUTOCOMPACTION")
1242 .map(|value| value != "false")
1243 .unwrap_or(true)
1244 {
1245 self.compact()?;
1246 }
1247
1248 Ok(filename)
1249 }
1250
1251 pub fn write_ref(
1255 &mut self,
1256 refname: &str,
1257 value: RefValue,
1258 log: Option<LogRecord>,
1259 opts: &WriteOptions,
1260 ) -> Result<()> {
1261 let update_index = self.max_update_index()? + 1;
1262 let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
1263
1264 writer.add_ref(RefRecord {
1268 name: refname.to_owned(),
1269 update_index,
1270 value,
1271 })?;
1272
1273 if let Some(log_rec) = log {
1274 let mut log_rec = log_rec;
1275 log_rec.update_index = update_index;
1276 writer.add_log(log_rec)?;
1277 }
1278
1279 let data = writer.finish()?;
1280 self.add_table(&data, update_index)?;
1281 Ok(())
1282 }
1283
1284 pub fn compact(&mut self) -> Result<()> {
1286 if self.table_names.len() <= 1 {
1287 return Ok(());
1288 }
1289
1290 let refs = self.read_refs()?;
1292 let logs = self.read_all_logs()?;
1293
1294 let mut min_idx = u64::MAX;
1296 let mut max_idx = 0u64;
1297 for name in &self.table_names {
1298 let path = self.reftable_dir.join(name);
1299 let data = fs::read(&path).map_err(Error::Io)?;
1300 let reader = ReftableReader::new(data)?;
1301 min_idx = min_idx.min(reader.min_update_index());
1302 max_idx = max_idx.max(reader.max_update_index());
1303 }
1304 if min_idx == u64::MAX {
1305 min_idx = 0;
1306 }
1307
1308 let mut writer = ReftableWriter::new(WriteOptions::default(), min_idx, max_idx);
1309 for rec in refs {
1310 writer.add_ref(rec)?;
1311 }
1312 for log in logs {
1313 writer.add_log(log)?;
1314 }
1315
1316 let data = writer.finish()?;
1317
1318 let old_names = self.table_names.clone();
1320 self.table_names.clear();
1321 let _name = self.add_table(&data, max_idx)?;
1322
1323 for old in &old_names {
1325 let path = self.reftable_dir.join(old);
1326 let _ = fs::remove_file(&path);
1327 }
1328
1329 Ok(())
1330 }
1331
1332 fn write_tables_list(&self) -> Result<()> {
1334 let tables_list = self.reftable_dir.join("tables.list");
1335 let lock = self.reftable_dir.join("tables.list.lock");
1336 let content = self.table_names.join("\n")
1337 + if self.table_names.is_empty() {
1338 ""
1339 } else {
1340 "\n"
1341 };
1342 fs::write(&lock, &content).map_err(Error::Io)?;
1343 fs::rename(&lock, &tables_list).map_err(Error::Io)?;
1344 Ok(())
1345 }
1346
1347 pub fn table_names(&self) -> &[String] {
1349 &self.table_names
1350 }
1351}
1352
1353pub fn is_reftable_repo(git_dir: &Path) -> bool {
1359 fn config_uses_reftable(config_path: &Path) -> bool {
1360 let Ok(content) = fs::read_to_string(config_path) else {
1361 return false;
1362 };
1363
1364 let mut in_extensions = false;
1365 for line in content.lines() {
1366 let trimmed = line.trim();
1367 if trimmed.starts_with('[') {
1368 in_extensions = trimmed.eq_ignore_ascii_case("[extensions]");
1369 continue;
1370 }
1371 if in_extensions {
1372 if let Some((key, value)) = trimmed.split_once('=') {
1373 if key.trim().eq_ignore_ascii_case("refstorage")
1374 && value.trim().eq_ignore_ascii_case("reftable")
1375 {
1376 return true;
1377 }
1378 }
1379 }
1380 }
1381 false
1382 }
1383
1384 let local_config = git_dir.join("config");
1385 if config_uses_reftable(&local_config) {
1386 return true;
1387 }
1388
1389 if let Ok(raw) = fs::read_to_string(git_dir.join("commondir")) {
1392 let rel = raw.trim();
1393 if !rel.is_empty() {
1394 let common = if Path::new(rel).is_absolute() {
1395 PathBuf::from(rel)
1396 } else {
1397 git_dir.join(rel)
1398 };
1399 let common_config = common.canonicalize().unwrap_or(common).join("config");
1400 if config_uses_reftable(&common_config) {
1401 return true;
1402 }
1403 }
1404 }
1405
1406 false
1407}
1408
1409pub fn reftable_resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
1411 reftable_resolve_ref_depth(git_dir, refname, 0)
1412}
1413
1414fn reftable_storage_location(git_dir: &Path, refname: &str) -> (PathBuf, String) {
1415 if let Some(rest) = refname.strip_prefix("worktrees/") {
1416 if let Some((worktree_id, per_worktree_ref)) = rest.split_once('/') {
1417 if per_worktree_ref.starts_with("refs/") {
1418 let common =
1419 crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
1420 return (
1421 common.join("worktrees").join(worktree_id),
1422 per_worktree_ref.to_owned(),
1423 );
1424 }
1425 }
1426 }
1427
1428 if refname == "HEAD"
1429 || refname.starts_with("refs/worktree/")
1430 || (git_dir.join("commondir").exists() && refname.starts_with("refs/bisect/"))
1431 {
1432 return (git_dir.to_path_buf(), refname.to_owned());
1433 }
1434
1435 (
1436 crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf()),
1437 refname.to_owned(),
1438 )
1439}
1440
1441fn reftable_resolve_ref_depth(git_dir: &Path, refname: &str, depth: usize) -> Result<ObjectId> {
1442 if depth > 10 {
1443 return Err(Error::InvalidRef(format!(
1444 "reftable: symlink too deep: {refname}"
1445 )));
1446 }
1447
1448 if refname == "HEAD" {
1450 let head_path = git_dir.join("HEAD");
1451 if head_path.exists() {
1452 let content = fs::read_to_string(&head_path).map_err(Error::Io)?;
1453 let content = content.trim();
1454 if let Some(target) = content.strip_prefix("ref: ") {
1455 if target.trim() == "refs/heads/.invalid" {
1456 return reftable_resolve_ref_depth(git_dir, "refs/worktree/HEAD", depth + 1);
1457 }
1458 return reftable_resolve_ref_depth(git_dir, target.trim(), depth + 1);
1459 }
1460 if content.len() == 40 && content.chars().all(|c| c.is_ascii_hexdigit()) {
1462 return content.parse();
1463 }
1464 }
1465 }
1466
1467 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1468 let stack = ReftableStack::open(&store_git_dir)?;
1469 match stack.lookup_ref(&storage_refname)? {
1470 Some(rec) => match rec.value {
1471 RefValue::Val1(oid) => Ok(oid),
1472 RefValue::Val2(oid, _) => Ok(oid),
1473 RefValue::Symref(target) => {
1474 reftable_resolve_ref_depth(&store_git_dir, &target, depth + 1)
1475 }
1476 RefValue::Deletion => Err(Error::InvalidRef(format!("ref not found: {refname}"))),
1477 },
1478 None => Err(Error::InvalidRef(format!("ref not found: {refname}"))),
1479 }
1480}
1481
1482pub fn reftable_write_ref(
1484 git_dir: &Path,
1485 refname: &str,
1486 oid: &ObjectId,
1487 log_identity: Option<&str>,
1488 log_message: Option<&str>,
1489) -> Result<()> {
1490 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1491 let mut stack = ReftableStack::open(&store_git_dir)?;
1492 let old_oid = stack
1493 .lookup_ref(&storage_refname)?
1494 .and_then(|r| match r.value {
1495 RefValue::Val1(oid) => Some(oid),
1496 RefValue::Val2(oid, _) => Some(oid),
1497 _ => None,
1498 })
1499 .unwrap_or_else(|| ObjectId::from_bytes(&[0u8; 20]).unwrap());
1500
1501 let log = if let Some(identity) = log_identity {
1502 let (name, email, time_secs, tz) = parse_identity_string(identity);
1503 Some(LogRecord {
1504 refname: storage_refname.clone(),
1505 update_index: 0, old_id: old_oid,
1507 new_id: *oid,
1508 name,
1509 email,
1510 time_seconds: time_secs,
1511 tz_offset: tz,
1512 message: log_message.unwrap_or("").to_owned(),
1513 })
1514 } else {
1515 None
1516 };
1517
1518 let write_log = log.is_some() || should_log_ref_updates(&store_git_dir);
1520 let log = if write_log { log } else { None };
1521
1522 let opts = read_write_options(&store_git_dir);
1523 stack.write_ref(&storage_refname, RefValue::Val1(*oid), log, &opts)
1524}
1525
1526pub fn reftable_write_symref(
1528 git_dir: &Path,
1529 refname: &str,
1530 target: &str,
1531 log_identity: Option<&str>,
1532 log_message: Option<&str>,
1533) -> Result<()> {
1534 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1535 let mut stack = ReftableStack::open(&store_git_dir)?;
1536 let opts = read_write_options(&store_git_dir);
1537
1538 let log = if let Some(identity) = log_identity {
1539 let (name, email, time_secs, tz) = parse_identity_string(identity);
1540 let zero_oid = ObjectId::from_bytes(&[0u8; 20])?;
1541 Some(LogRecord {
1542 refname: storage_refname.clone(),
1543 update_index: 0,
1544 old_id: zero_oid,
1545 new_id: zero_oid,
1546 name,
1547 email,
1548 time_seconds: time_secs,
1549 tz_offset: tz,
1550 message: log_message.unwrap_or("").to_owned(),
1551 })
1552 } else {
1553 None
1554 };
1555
1556 stack.write_ref(
1557 &storage_refname,
1558 RefValue::Symref(target.to_owned()),
1559 log,
1560 &opts,
1561 )
1562}
1563
1564pub fn reftable_delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
1566 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1567 let mut stack = ReftableStack::open(&store_git_dir)?;
1568 let opts = read_write_options(&store_git_dir);
1569 stack.write_ref(&storage_refname, RefValue::Deletion, None, &opts)
1570}
1571
1572pub fn reftable_read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
1574 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1575 let stack = ReftableStack::open(&store_git_dir)?;
1576 match stack.lookup_ref(&storage_refname)? {
1577 Some(rec) => match rec.value {
1578 RefValue::Symref(target) => Ok(Some(target)),
1579 _ => Ok(None),
1580 },
1581 None => Ok(None),
1582 }
1583}
1584
1585pub fn reftable_list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
1587 let stack = ReftableStack::open(git_dir)?;
1588 let refs = stack.read_refs()?;
1589 let mut result = Vec::new();
1590 for rec in refs {
1591 let matches_prefix = rec.name.starts_with(prefix)
1592 || (prefix.ends_with('/') && rec.name == prefix.trim_end_matches('/'));
1593 if matches_prefix {
1594 match rec.value {
1595 RefValue::Val1(oid) => result.push((rec.name, oid)),
1596 RefValue::Val2(oid, _) => result.push((rec.name, oid)),
1597 RefValue::Symref(target) => {
1598 if let Ok(oid) = reftable_resolve_ref(git_dir, &target) {
1600 result.push((rec.name, oid));
1601 }
1602 }
1603 RefValue::Deletion => {}
1604 }
1605 }
1606 }
1607 result.sort_by(|a, b| a.0.cmp(&b.0));
1608 Ok(result)
1609}
1610
1611pub fn reftable_read_reflog(
1613 git_dir: &Path,
1614 refname: &str,
1615) -> Result<Vec<crate::reflog::ReflogEntry>> {
1616 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1617 let stack = ReftableStack::open(&store_git_dir)?;
1618 let logs = stack.read_logs_for_ref(&storage_refname)?;
1619 let mut entries = Vec::new();
1620 for log in logs {
1621 let tz_sign = if log.tz_offset >= 0 { '+' } else { '-' };
1623 let tz_abs = log.tz_offset.unsigned_abs();
1624 let tz_hours = tz_abs / 60;
1625 let tz_mins = tz_abs % 60;
1626 let identity = format!(
1627 "{} <{}> {} {}{:02}{:02}",
1628 log.name, log.email, log.time_seconds, tz_sign, tz_hours, tz_mins
1629 );
1630 entries.push(crate::reflog::ReflogEntry {
1631 old_oid: log.old_id,
1632 new_oid: log.new_id,
1633 identity,
1634 message: log.message,
1635 });
1636 }
1637 Ok(entries)
1638}
1639
1640pub fn reftable_append_reflog(
1642 git_dir: &Path,
1643 refname: &str,
1644 old_oid: &ObjectId,
1645 new_oid: &ObjectId,
1646 identity: &str,
1647 message: &str,
1648 force_create: bool,
1649) -> Result<()> {
1650 use crate::refs::should_autocreate_reflog;
1651 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1652 if !force_create
1653 && !should_autocreate_reflog(&store_git_dir, &storage_refname)
1654 && message.is_empty()
1655 && !reftable_reflog_exists(&store_git_dir, &storage_refname)
1656 {
1657 return Ok(());
1658 }
1659 let (name, email, time_secs, tz) = parse_identity_string(identity);
1660 let mut stack = ReftableStack::open(&store_git_dir)?;
1661 let update_index = stack.max_update_index()? + 1;
1662 let opts = read_write_options(&store_git_dir);
1663
1664 let mut writer = ReftableWriter::new(opts, update_index, update_index);
1665 writer.add_log(LogRecord {
1666 refname: storage_refname,
1667 update_index,
1668 old_id: *old_oid,
1669 new_id: *new_oid,
1670 name,
1671 email,
1672 time_seconds: time_secs,
1673 tz_offset: tz,
1674 message: message.to_owned(),
1675 })?;
1676
1677 let data = writer.finish()?;
1678 stack.add_table(&data, update_index)?;
1679 Ok(())
1680}
1681
1682pub fn reftable_reflog_exists(git_dir: &Path, refname: &str) -> bool {
1684 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1685 if let Ok(stack) = ReftableStack::open(&store_git_dir) {
1686 if let Ok(logs) = stack.read_logs_for_ref(&storage_refname) {
1687 return !logs.is_empty();
1688 }
1689 }
1690 false
1691}
1692
1693pub fn read_write_options(git_dir: &Path) -> WriteOptions {
1699 let mut opts = WriteOptions::default();
1700
1701 if let Ok(config) = ConfigSet::load(Some(git_dir), true) {
1702 if let Some(value) = config.get("reftable.blockSize") {
1703 if let Ok(v) = value.parse::<u32>() {
1704 opts.block_size = v;
1705 }
1706 }
1707 if let Some(value) = config.get("reftable.restartInterval") {
1708 if let Ok(v) = value.parse::<usize>() {
1709 opts.restart_interval = v;
1710 }
1711 }
1712 if let Some(value) = config.get("core.logAllRefUpdates") {
1713 let value = value.to_lowercase();
1714 if !(value == "true" || value == "always") {
1715 opts.write_log = false;
1716 }
1717 }
1718 return opts;
1719 }
1720
1721 let config_path = git_dir.join("config");
1722 if let Ok(content) = fs::read_to_string(&config_path) {
1723 let mut in_reftable = false;
1724 let mut in_core = false;
1725 let mut log_all_ref_updates: Option<bool> = None;
1726
1727 for line in content.lines() {
1728 let trimmed = line.trim();
1729 if trimmed.starts_with('[') {
1730 let section_lower = trimmed.to_lowercase();
1731 in_reftable = section_lower.starts_with("[reftable]");
1732 in_core = section_lower.starts_with("[core]");
1733 continue;
1734 }
1735 if in_reftable {
1736 if let Some((key, value)) = trimmed.split_once('=') {
1737 let key = key.trim().to_lowercase();
1738 let value = value.trim();
1739 match key.as_str() {
1740 "blocksize" => {
1741 if let Ok(v) = value.parse::<u32>() {
1742 opts.block_size = v;
1743 }
1744 }
1745 "restartinterval" => {
1746 if let Ok(v) = value.parse::<usize>() {
1747 opts.restart_interval = v;
1748 }
1749 }
1750 _ => {}
1751 }
1752 }
1753 }
1754 if in_core {
1755 if let Some((key, value)) = trimmed.split_once('=') {
1756 let key = key.trim().to_lowercase();
1757 let value = value.trim().to_lowercase();
1758 if key == "logallrefupdates" {
1759 log_all_ref_updates = Some(value == "true" || value == "always");
1760 }
1761 }
1762 }
1763 }
1764
1765 if let Some(false) = log_all_ref_updates {
1766 opts.write_log = false;
1767 }
1768 }
1769
1770 opts
1771}
1772
1773fn should_log_ref_updates(git_dir: &Path) -> bool {
1775 let config_path = git_dir.join("config");
1776 if let Ok(content) = fs::read_to_string(&config_path) {
1777 let mut in_core = false;
1778 for line in content.lines() {
1779 let trimmed = line.trim();
1780 if trimmed.starts_with('[') {
1781 in_core = trimmed.to_lowercase().starts_with("[core]");
1782 continue;
1783 }
1784 if in_core {
1785 if let Some((key, value)) = trimmed.split_once('=') {
1786 if key.trim().eq_ignore_ascii_case("logallrefupdates") {
1787 let v = value.trim().to_lowercase();
1788 return v == "true" || v == "always";
1789 }
1790 }
1791 }
1792 }
1793 }
1794 false
1795}
1796
1797fn crc32(data: &[u8]) -> u32 {
1803 let mut crc: u32 = 0xffffffff;
1804 for &byte in data {
1805 crc ^= byte as u32;
1806 for _ in 0..8 {
1807 if crc & 1 != 0 {
1808 crc = (crc >> 1) ^ 0xedb88320;
1809 } else {
1810 crc >>= 1;
1811 }
1812 }
1813 }
1814 !crc
1815}
1816
1817fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
1819 a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
1820}
1821
1822fn read_u24(data: &[u8], pos: usize) -> usize {
1824 ((data[pos] as usize) << 16) | ((data[pos + 1] as usize) << 8) | (data[pos + 2] as usize)
1825}
1826
1827fn read_u16(data: &[u8], pos: usize) -> usize {
1829 ((data[pos] as usize) << 8) | (data[pos + 1] as usize)
1830}
1831
1832fn parse_footer(data: &[u8], version: u8) -> Result<Footer> {
1834 let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
1835 if data.len() < footer_size {
1836 return Err(Error::InvalidRef("reftable: footer too small".into()));
1837 }
1838
1839 if &data[0..4] != REFTABLE_MAGIC {
1841 return Err(Error::InvalidRef("reftable: bad footer magic".into()));
1842 }
1843 let fver = data[4];
1844 if fver != version {
1845 return Err(Error::InvalidRef(format!(
1846 "reftable: footer version mismatch: header={version}, footer={fver}"
1847 )));
1848 }
1849
1850 let block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
1851 let min_update_index = u64::from_be_bytes(data[8..16].try_into().unwrap());
1852 let max_update_index = u64::from_be_bytes(data[16..24].try_into().unwrap());
1853
1854 let off = 24;
1855 let ref_index_position = u64::from_be_bytes(data[off..off + 8].try_into().unwrap());
1856 let obj_position_and_id_len = u64::from_be_bytes(data[off + 8..off + 16].try_into().unwrap());
1857 let obj_index_position = u64::from_be_bytes(data[off + 16..off + 24].try_into().unwrap());
1858 let log_position = u64::from_be_bytes(data[off + 24..off + 32].try_into().unwrap());
1859 let log_index_position = u64::from_be_bytes(data[off + 32..off + 40].try_into().unwrap());
1860
1861 let crc_stored = u32::from_be_bytes(data[footer_size - 4..footer_size].try_into().unwrap());
1863 let crc_computed = crc32(&data[..footer_size - 4]);
1864 if crc_stored != crc_computed {
1865 return Err(Error::InvalidRef(format!(
1866 "reftable: footer CRC mismatch: stored={crc_stored:08x}, computed={crc_computed:08x}"
1867 )));
1868 }
1869
1870 Ok(Footer {
1871 version: fver,
1872 block_size,
1873 min_update_index,
1874 max_update_index,
1875 ref_index_position,
1876 obj_position_and_id_len,
1877 obj_index_position,
1878 log_position,
1879 log_index_position,
1880 })
1881}
1882
1883fn parse_identity_string(identity: &str) -> (String, String, u64, i16) {
1885 let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
1887 if parts.len() < 3 {
1888 return (identity.to_owned(), String::new(), 0, 0);
1889 }
1890 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);
1895
1896 let tz_minutes = if tz_str.len() >= 5 {
1898 let sign = if tz_str.starts_with('-') { -1i16 } else { 1 };
1899 let hours = tz_str[1..3].parse::<i16>().unwrap_or(0);
1900 let mins = tz_str[3..5].parse::<i16>().unwrap_or(0);
1901 sign * (hours * 60 + mins)
1902 } else {
1903 0
1904 };
1905
1906 let (name, email) = if let Some(lt_pos) = name_email.find('<') {
1908 let name = name_email[..lt_pos].trim().to_owned();
1909 let email = if let Some(gt_pos) = name_email.find('>') {
1910 name_email[lt_pos + 1..gt_pos].to_owned()
1911 } else {
1912 name_email[lt_pos + 1..].to_owned()
1913 };
1914 (name, email)
1915 } else {
1916 (name_email.to_owned(), String::new())
1917 };
1918
1919 (name, email, time_secs, tz_minutes)
1920}
1921
1922#[cfg(test)]
1927mod tests {
1928 use super::*;
1929
1930 #[test]
1931 fn test_varint_roundtrip() {
1932 for val in [0u64, 1, 127, 128, 255, 256, 16383, 16384, u64::MAX] {
1933 let mut buf = Vec::new();
1934 put_varint(val, &mut buf);
1935 let (decoded, end) = get_varint(&buf, 0).unwrap();
1936 assert_eq!(decoded, val, "varint roundtrip failed for {val}");
1937 assert_eq!(end, buf.len());
1938 }
1939 }
1940
1941 #[test]
1942 fn test_crc32() {
1943 assert_eq!(crc32(b"123456789"), 0xCBF43926);
1945 }
1946
1947 #[test]
1948 fn test_empty_table() {
1949 let writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
1950 let data = writer.finish().unwrap();
1951 let reader = ReftableReader::new(data).unwrap();
1952 let refs = reader.read_refs().unwrap();
1953 assert!(refs.is_empty());
1954 }
1955
1956 #[test]
1957 fn test_write_read_single_ref() {
1958 let oid = ObjectId::from_bytes(&[0xab; 20]).unwrap();
1959 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
1960 writer
1961 .add_ref(RefRecord {
1962 name: "refs/heads/main".to_owned(),
1963 update_index: 1,
1964 value: RefValue::Val1(oid),
1965 })
1966 .unwrap();
1967 let data = writer.finish().unwrap();
1968
1969 let reader = ReftableReader::new(data).unwrap();
1970 let refs = reader.read_refs().unwrap();
1971 assert_eq!(refs.len(), 1);
1972 assert_eq!(refs[0].name, "refs/heads/main");
1973 assert_eq!(refs[0].value, RefValue::Val1(oid));
1974 assert_eq!(refs[0].update_index, 1);
1975 }
1976
1977 #[test]
1978 fn test_write_read_multiple_refs() {
1979 let oid1 = ObjectId::from_bytes(&[0x11; 20]).unwrap();
1980 let oid2 = ObjectId::from_bytes(&[0x22; 20]).unwrap();
1981 let oid3 = ObjectId::from_bytes(&[0x33; 20]).unwrap();
1982
1983 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
1984 writer
1985 .add_ref(RefRecord {
1986 name: "refs/heads/a".to_owned(),
1987 update_index: 1,
1988 value: RefValue::Val1(oid1),
1989 })
1990 .unwrap();
1991 writer
1992 .add_ref(RefRecord {
1993 name: "refs/heads/b".to_owned(),
1994 update_index: 1,
1995 value: RefValue::Val1(oid2),
1996 })
1997 .unwrap();
1998 writer
1999 .add_ref(RefRecord {
2000 name: "refs/tags/v1.0".to_owned(),
2001 update_index: 1,
2002 value: RefValue::Val2(oid3, oid1),
2003 })
2004 .unwrap();
2005 let data = writer.finish().unwrap();
2006
2007 let reader = ReftableReader::new(data).unwrap();
2008 let refs = reader.read_refs().unwrap();
2009 assert_eq!(refs.len(), 3);
2010 assert_eq!(refs[0].name, "refs/heads/a");
2011 assert_eq!(refs[1].name, "refs/heads/b");
2012 assert_eq!(refs[2].name, "refs/tags/v1.0");
2013 assert_eq!(refs[2].value, RefValue::Val2(oid3, oid1));
2014 }
2015
2016 #[test]
2017 fn test_symref_roundtrip() {
2018 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
2019 writer
2020 .add_ref(RefRecord {
2021 name: "refs/heads/sym".to_owned(),
2022 update_index: 1,
2023 value: RefValue::Symref("refs/heads/main".to_owned()),
2024 })
2025 .unwrap();
2026 let data = writer.finish().unwrap();
2027
2028 let reader = ReftableReader::new(data).unwrap();
2029 let refs = reader.read_refs().unwrap();
2030 assert_eq!(refs.len(), 1);
2031 assert_eq!(
2032 refs[0].value,
2033 RefValue::Symref("refs/heads/main".to_owned())
2034 );
2035 }
2036
2037 #[test]
2038 fn test_log_roundtrip() {
2039 let old_oid = ObjectId::from_bytes(&[0; 20]).unwrap();
2040 let new_oid = ObjectId::from_bytes(&[0xaa; 20]).unwrap();
2041
2042 let mut opts = WriteOptions::default();
2043 opts.write_log = true;
2044 let mut writer = ReftableWriter::new(opts, 1, 1);
2045 writer
2046 .add_log(LogRecord {
2047 refname: "refs/heads/main".to_owned(),
2048 update_index: 1,
2049 old_id: old_oid,
2050 new_id: new_oid,
2051 name: "Test User".to_owned(),
2052 email: "test@example.com".to_owned(),
2053 time_seconds: 1700000000,
2054 tz_offset: -480,
2055 message: "initial commit".to_owned(),
2056 })
2057 .unwrap();
2058 let data = writer.finish().unwrap();
2059
2060 let reader = ReftableReader::new(data).unwrap();
2061 let logs = reader.read_logs().unwrap();
2062 assert_eq!(logs.len(), 1);
2063 assert_eq!(logs[0].refname, "refs/heads/main");
2064 assert_eq!(logs[0].old_id, old_oid);
2065 assert_eq!(logs[0].new_id, new_oid);
2066 assert_eq!(logs[0].name, "Test User");
2067 assert_eq!(logs[0].email, "test@example.com");
2068 assert_eq!(logs[0].time_seconds, 1700000000);
2069 assert_eq!(logs[0].tz_offset, -480);
2070 assert_eq!(logs[0].message, "initial commit");
2071 }
2072
2073 #[test]
2074 fn test_unaligned_table() {
2075 let oid = ObjectId::from_bytes(&[0xcc; 20]).unwrap();
2076 let opts = WriteOptions {
2077 block_size: 0, restart_interval: 16,
2079 write_log: false,
2080 };
2081 let mut writer = ReftableWriter::new(opts, 1, 1);
2082 writer
2083 .add_ref(RefRecord {
2084 name: "refs/heads/main".to_owned(),
2085 update_index: 1,
2086 value: RefValue::Val1(oid),
2087 })
2088 .unwrap();
2089 let data = writer.finish().unwrap();
2090
2091 let reader = ReftableReader::new(data).unwrap();
2092 assert_eq!(reader.block_size(), 0);
2093 let refs = reader.read_refs().unwrap();
2094 assert_eq!(refs.len(), 1);
2095 assert_eq!(refs[0].value, RefValue::Val1(oid));
2096 }
2097
2098 #[test]
2099 fn test_parse_identity() {
2100 let (name, email, ts, tz) =
2101 parse_identity_string("Test User <test@example.com> 1700000000 -0800");
2102 assert_eq!(name, "Test User");
2103 assert_eq!(email, "test@example.com");
2104 assert_eq!(ts, 1700000000);
2105 assert_eq!(tz, -480);
2106 }
2107
2108 #[test]
2109 fn test_deletion_record() {
2110 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
2111 writer
2112 .add_ref(RefRecord {
2113 name: "refs/heads/gone".to_owned(),
2114 update_index: 1,
2115 value: RefValue::Deletion,
2116 })
2117 .unwrap();
2118 let data = writer.finish().unwrap();
2119
2120 let reader = ReftableReader::new(data).unwrap();
2121 let refs = reader.read_refs().unwrap();
2122 assert_eq!(refs.len(), 1);
2123 assert_eq!(refs[0].value, RefValue::Deletion);
2124 }
2125}