1#[cfg(unix)]
5use std::os::unix::fs::MetadataExt;
6#[cfg(unix)]
7use std::time::Duration;
8use std::{
9 collections::BTreeMap,
10 fmt::{Display, Formatter},
11 fs::{self, File},
12 io,
13 io::{BufReader, Read, Write},
14 path::{Path, PathBuf},
15 time::{SystemTime, UNIX_EPOCH},
16};
17
18use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
19
20use crate::{
21 errors::GitError,
22 hash::{ObjectHash, get_hash_kind},
23 internal::pack::wrapper::Wrapper,
24 utils::{self, HashAlgorithm},
25};
26
27#[derive(PartialEq, Eq, Debug, Clone)]
29pub struct Time {
30 seconds: u32,
31 nanos: u32,
32}
33impl Time {
34 pub fn from_stream(stream: &mut impl Read) -> Result<Self, GitError> {
36 let seconds = stream.read_u32::<BigEndian>()?;
37 let nanos = stream.read_u32::<BigEndian>()?;
38 Ok(Time { seconds, nanos })
39 }
40
41 #[allow(dead_code)]
43 fn to_system_time(&self) -> SystemTime {
44 UNIX_EPOCH + std::time::Duration::new(self.seconds.into(), self.nanos)
45 }
46
47 pub fn from_system_time(system_time: SystemTime) -> Self {
49 match system_time.duration_since(UNIX_EPOCH) {
50 Ok(duration) => {
51 let seconds = duration
52 .as_secs()
53 .try_into()
54 .expect("Time is too far in the future");
55 let nanos = duration.subsec_nanos();
56 Time { seconds, nanos }
57 }
58 Err(_) => panic!("Time is before the UNIX epoch"),
59 }
60 }
61}
62impl Display for Time {
63 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
64 write!(f, "{}:{}", self.seconds, self.nanos)
65 }
66}
67
68#[cfg(unix)]
69fn index_ctime(meta: &fs::Metadata) -> SystemTime {
70 unix_metadata_time(meta.ctime(), meta.ctime_nsec())
71}
72
73#[cfg(not(unix))]
74fn index_ctime(meta: &fs::Metadata) -> SystemTime {
75 meta.created()
76 .or_else(|_| meta.modified())
77 .unwrap_or(UNIX_EPOCH)
78}
79
80#[cfg(unix)]
81fn index_mtime(meta: &fs::Metadata) -> SystemTime {
82 unix_metadata_time(meta.mtime(), meta.mtime_nsec())
83}
84
85#[cfg(not(unix))]
86fn index_mtime(meta: &fs::Metadata) -> SystemTime {
87 meta.modified()
88 .or_else(|_| meta.created())
89 .unwrap_or(UNIX_EPOCH)
90}
91
92#[cfg(unix)]
93fn unix_metadata_time(seconds: i64, nanos: i64) -> SystemTime {
94 if seconds < 0 {
95 return UNIX_EPOCH;
96 }
97
98 let nanos = u32::try_from(nanos)
99 .ok()
100 .filter(|nanos| *nanos < 1_000_000_000)
101 .unwrap_or(0);
102
103 UNIX_EPOCH + Duration::new(seconds as u64, nanos)
104}
105
106#[derive(Debug)]
108pub struct Flags {
109 pub assume_valid: bool,
110 pub extended: bool, pub stage: u8, pub name_length: u16, }
114
115impl From<u16> for Flags {
116 fn from(flags: u16) -> Self {
117 Flags {
118 assume_valid: flags & 0x8000 != 0,
119 extended: flags & 0x4000 != 0,
120 stage: ((flags & 0x3000) >> 12) as u8,
121 name_length: flags & 0xFFF,
122 }
123 }
124}
125
126impl TryInto<u16> for &Flags {
127 type Error = &'static str;
128 fn try_into(self) -> Result<u16, Self::Error> {
129 let mut flags = 0u16;
130 if self.assume_valid {
131 flags |= 0x8000; }
133 if self.extended {
134 flags |= 0x4000; }
136 flags |= (self.stage as u16) << 12; if self.name_length > 0xFFF {
138 return Err("Name length is too long");
139 }
140 flags |= self.name_length; Ok(flags)
142 }
143}
144
145impl Flags {
146 pub fn new(name_len: u16) -> Self {
147 Flags {
148 assume_valid: true,
149 extended: false,
150 stage: 0,
151 name_length: name_len,
152 }
153 }
154}
155
156pub struct IndexEntry {
158 pub ctime: Time,
159 pub mtime: Time,
160 pub dev: u32, pub ino: u32, pub mode: u32, pub uid: u32, pub gid: u32, pub size: u32,
166 pub hash: ObjectHash,
167 pub flags: Flags,
168 pub name: String,
169}
170impl Display for IndexEntry {
171 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
172 write!(
173 f,
174 "IndexEntry {{ ctime: {}, mtime: {}, dev: {}, ino: {}, mode: {:o}, uid: {}, gid: {}, size: {}, hash: {}, flags: {:?}, name: {} }}",
175 self.ctime,
176 self.mtime,
177 self.dev,
178 self.ino,
179 self.mode,
180 self.uid,
181 self.gid,
182 self.size,
183 self.hash,
184 self.flags,
185 self.name
186 )
187 }
188}
189
190impl IndexEntry {
191 pub fn new(meta: &fs::Metadata, hash: ObjectHash, name: String) -> Self {
193 let mut entry = IndexEntry {
194 ctime: Time::from_system_time(index_ctime(meta)),
195 mtime: Time::from_system_time(index_mtime(meta)),
196 dev: 0,
197 ino: 0,
198 uid: 0,
199 gid: 0,
200 size: meta.len() as u32,
201 hash,
202 flags: Flags::new(name.len() as u16),
203 name,
204 mode: 0o100644,
205 };
206 #[cfg(unix)]
207 {
208 entry.dev = meta.dev() as u32;
209 entry.ino = meta.ino() as u32;
210 entry.uid = meta.uid();
211 entry.gid = meta.gid();
212
213 entry.mode = match meta.mode() & 0o170000{
214 0o100000 => {
215 match meta.mode() & 0o111 {
216 0 => 0o100644, _ => 0o100755, }
219 }
220 0o120000 => 0o120000, _ => entry.mode, }
223 }
224 #[cfg(windows)]
225 {
226 if meta.is_symlink() {
227 entry.mode = 0o120000;
228 }
229 }
230 entry
231 }
232
233 pub fn new_from_file(file: &Path, hash: ObjectHash, workdir: &Path) -> io::Result<Self> {
236 let name = file.to_str().unwrap().to_string();
237 let file_abs = workdir.join(file);
238 let meta = fs::symlink_metadata(file_abs)?; let index = IndexEntry::new(&meta, hash, name);
240 Ok(index)
241 }
242
243 pub fn new_from_blob(name: String, hash: ObjectHash, size: u32) -> Self {
245 IndexEntry {
246 ctime: Time {
247 seconds: 0,
248 nanos: 0,
249 },
250 mtime: Time {
251 seconds: 0,
252 nanos: 0,
253 },
254 dev: 0,
255 ino: 0,
256 mode: 0o100644,
257 uid: 0,
258 gid: 0,
259 size,
260 hash,
261 flags: Flags::new(name.len() as u16),
262 name,
263 }
264 }
265}
266
267pub struct Index {
270 entries: BTreeMap<(String, u8), IndexEntry>,
271}
272
273impl Index {
274 pub fn new() -> Self {
275 Index {
276 entries: BTreeMap::new(),
277 }
278 }
279
280 fn check_header(file: &mut impl Read) -> Result<u32, GitError> {
281 let mut magic = [0; 4];
282 file.read_exact(&mut magic)?;
283 if magic != *b"DIRC" {
284 return Err(GitError::InvalidIndexHeader(
285 String::from_utf8_lossy(&magic).to_string(),
286 ));
287 }
288
289 let version = file.read_u32::<BigEndian>()?;
290 if version != 2 {
292 return Err(GitError::InvalidIndexHeader(version.to_string()));
293 }
294
295 let entries = file.read_u32::<BigEndian>()?;
296 Ok(entries)
297 }
298
299 pub fn size(&self) -> usize {
300 self.entries.len()
301 }
302
303 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, GitError> {
304 let file = File::open(path.as_ref())?; let total_size = file.metadata()?.len();
306 let file = &mut Wrapper::new(BufReader::new(file)); let num = Index::check_header(file)?;
309 let mut index = Index::new();
310
311 for _ in 0..num {
312 let mut entry = IndexEntry {
313 ctime: Time::from_stream(file)?,
314 mtime: Time::from_stream(file)?,
315 dev: file.read_u32::<BigEndian>()?, ino: file.read_u32::<BigEndian>()?,
317 mode: file.read_u32::<BigEndian>()?,
318 uid: file.read_u32::<BigEndian>()?,
319 gid: file.read_u32::<BigEndian>()?,
320 size: file.read_u32::<BigEndian>()?,
321 hash: utils::read_sha(file)?,
322 flags: Flags::from(file.read_u16::<BigEndian>()?),
323 name: String::new(),
324 };
325 let name_len = entry.flags.name_length as usize;
326 let mut name = vec![0; name_len];
327 file.read_exact(&mut name)?;
328 entry.name =
330 String::from_utf8(name).map_err(|e| GitError::ConversionError(e.to_string()))?; index
332 .entries
333 .insert((entry.name.clone(), entry.flags.stage), entry);
334
335 let hash_len = get_hash_kind().size();
338 let entry_len = hash_len + 2 + name_len;
339 let padding = 1 + ((8 - ((entry_len + 1) % 8)) % 8); utils::read_bytes(file, padding)?;
341 }
342
343 while file.bytes_read() + get_hash_kind().size() < total_size as usize {
345 let sign = utils::read_bytes(file, 4)?;
347 println!(
348 "{:?}",
349 String::from_utf8(sign.clone())
350 .map_err(|e| GitError::ConversionError(e.to_string()))?
351 );
352 if sign[0] >= b'A' && sign[0] <= b'Z' {
354 let size = file.read_u32::<BigEndian>()?;
356 utils::read_bytes(file, size as usize)?; } else {
358 return Err(GitError::InvalidIndexFile(
360 "Unsupported extension".to_string(),
361 ));
362 }
363 }
364
365 let file_hash = file.final_hash();
367 let check_sum = utils::read_sha(file)?;
368 if file_hash != check_sum {
369 return Err(GitError::InvalidIndexFile("Check sum failed".to_string()));
370 }
371 assert_eq!(index.size(), num as usize);
372 Ok(index)
373 }
374
375 pub fn to_file(&self, path: impl AsRef<Path>) -> Result<(), GitError> {
376 let mut file = File::create(path)?;
377 let mut hash = HashAlgorithm::new();
378
379 let mut header = Vec::new();
380 header.write_all(b"DIRC")?;
381 header.write_u32::<BigEndian>(2u32)?; header.write_u32::<BigEndian>(self.entries.len() as u32)?;
383 file.write_all(&header)?;
384 hash.update(&header);
385
386 for entry in self.entries.values() {
387 let mut entry_bytes = Vec::new();
388 entry_bytes.write_u32::<BigEndian>(entry.ctime.seconds)?;
389 entry_bytes.write_u32::<BigEndian>(entry.ctime.nanos)?;
390 entry_bytes.write_u32::<BigEndian>(entry.mtime.seconds)?;
391 entry_bytes.write_u32::<BigEndian>(entry.mtime.nanos)?;
392 entry_bytes.write_u32::<BigEndian>(entry.dev)?;
393 entry_bytes.write_u32::<BigEndian>(entry.ino)?;
394 entry_bytes.write_u32::<BigEndian>(entry.mode)?;
395 entry_bytes.write_u32::<BigEndian>(entry.uid)?;
396 entry_bytes.write_u32::<BigEndian>(entry.gid)?;
397 entry_bytes.write_u32::<BigEndian>(entry.size)?;
398 entry_bytes.write_all(entry.hash.as_ref())?;
399 entry_bytes.write_u16::<BigEndian>((&entry.flags).try_into().unwrap())?;
400 entry_bytes.write_all(entry.name.as_bytes())?;
401 let hash_len = get_hash_kind().size();
402 let entry_len = hash_len + 2 + entry.name.len();
403 let padding = 1 + ((8 - ((entry_len + 1) % 8)) % 8); entry_bytes.write_all(&vec![0; padding])?;
405 file.write_all(&entry_bytes)?;
406 hash.update(&entry_bytes);
407 }
408
409 let file_hash =
413 ObjectHash::from_bytes(&hash.finalize()).map_err(GitError::InvalidIndexFile)?;
414 file.write_all(file_hash.as_ref())?;
415 Ok(())
416 }
417
418 pub fn refresh(&mut self, file: impl AsRef<Path>, workdir: &Path) -> Result<bool, GitError> {
419 let path = file.as_ref();
420 let name = path
421 .to_str()
422 .ok_or(GitError::InvalidPathError(format!("{path:?}")))?;
423
424 if let Some(entry) = self.entries.get_mut(&(name.to_string(), 0)) {
425 let abs_path = workdir.join(path);
426 let meta = fs::symlink_metadata(&abs_path)?;
427 let new_ctime = Time::from_system_time(index_ctime(&meta));
428 let new_mtime = Time::from_system_time(index_mtime(&meta));
429 let new_size = meta.len() as u32;
430
431 let mut file = File::open(&abs_path)?;
433 let mut hasher = HashAlgorithm::new();
434 io::copy(&mut file, &mut hasher)?;
435 let new_hash = ObjectHash::from_bytes(&hasher.finalize()).unwrap();
436
437 if entry.ctime != new_ctime
439 || entry.mtime != new_mtime
440 || entry.size != new_size
441 || entry.hash != new_hash
442 {
443 entry.ctime = new_ctime;
444 entry.mtime = new_mtime;
445 entry.size = new_size;
446 entry.hash = new_hash;
447 return Ok(true);
448 }
449 }
450 Ok(false)
451 }
452}
453
454impl Default for Index {
455 fn default() -> Self {
456 Self::new()
457 }
458}
459
460impl Index {
461 pub fn load(index_file: impl AsRef<Path>) -> Result<Self, GitError> {
463 let path = index_file.as_ref();
464 if !path.exists() {
465 return Ok(Index::new());
466 }
467 Index::from_file(path)
468 }
469
470 pub fn update(&mut self, entry: IndexEntry) {
471 self.add(entry)
472 }
473
474 pub fn add(&mut self, entry: IndexEntry) {
475 self.entries
476 .insert((entry.name.clone(), entry.flags.stage), entry);
477 }
478
479 pub fn remove(&mut self, name: &str, stage: u8) -> Option<IndexEntry> {
480 self.entries.remove(&(name.to_string(), stage))
481 }
482
483 pub fn get(&self, name: &str, stage: u8) -> Option<&IndexEntry> {
484 self.entries.get(&(name.to_string(), stage))
485 }
486
487 pub fn tracked(&self, name: &str, stage: u8) -> bool {
488 self.entries.contains_key(&(name.to_string(), stage))
489 }
490
491 pub fn get_hash(&self, file: &str, stage: u8) -> Option<ObjectHash> {
492 self.get(file, stage).map(|entry| entry.hash)
493 }
494
495 pub fn verify_hash(&self, file: &str, stage: u8, hash: &ObjectHash) -> bool {
496 let inner_hash = self.get_hash(file, stage);
497 if let Some(inner_hash) = inner_hash {
498 &inner_hash == hash
499 } else {
500 false
501 }
502 }
503 pub fn is_modified(&self, file: &str, stage: u8, workdir: &Path) -> bool {
506 if let Some(entry) = self.get(file, stage) {
507 let path_abs = workdir.join(file);
508 let meta = path_abs.symlink_metadata().unwrap();
509 let same = entry.ctime == Time::from_system_time(index_ctime(&meta))
511 && entry.mtime == Time::from_system_time(index_mtime(&meta))
512 && entry.size == meta.len() as u32;
513
514 !same
515 } else {
516 panic!("File not found in index");
517 }
518 }
519
520 pub fn tracked_entries(&self, stage: u8) -> Vec<&IndexEntry> {
522 self.entries
524 .iter()
525 .filter(|(_, entry)| entry.flags.stage == stage)
526 .map(|(_, entry)| entry)
527 .collect()
528 }
529
530 pub fn tracked_files(&self) -> Vec<PathBuf> {
532 self.tracked_entries(0)
533 .iter()
534 .map(|entry| PathBuf::from(&entry.name))
535 .collect()
536 }
537
538 pub fn contains_dir_file(&self, dir: &str) -> bool {
541 let dir = Path::new(dir);
542 self.entries.iter().any(|((name, _), _)| {
543 let path = Path::new(name);
544 path.starts_with(dir) && path != dir })
546 }
547
548 pub fn remove_dir_files(&mut self, dir: &str) -> Vec<String> {
551 let dir = Path::new(dir);
552 let mut removed = Vec::new();
553 self.entries.retain(|(name, _), _| {
554 let path = Path::new(name);
555 if path.starts_with(dir) && path != dir {
556 removed.push(name.clone());
557 false
558 } else {
559 true
560 }
561 });
562 removed
563 }
564
565 pub fn save(&self, index_file: impl AsRef<Path>) -> Result<(), GitError> {
567 self.to_file(index_file)
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use std::io::Cursor;
574
575 use super::*;
576 use crate::hash::{HashKind, set_hash_kind_for_test};
577
578 #[test]
580 fn test_time() {
581 let time = Time {
582 seconds: 0,
583 nanos: 0,
584 };
585 let system_time = time.to_system_time();
586 let new_time = Time::from_system_time(system_time);
587 assert_eq!(time, new_time);
588 }
589
590 #[test]
592 fn test_check_header() {
593 let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
594 source.push("tests/data/index/index-2");
595
596 let file = File::open(source).unwrap();
597 let entries = Index::check_header(&mut BufReader::new(file)).unwrap();
598 assert_eq!(entries, 2);
599 }
600
601 #[test]
603 fn test_index() {
604 let _guard = set_hash_kind_for_test(HashKind::Sha1);
605 let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
606 source.push("tests/data/index/index-760");
607
608 let index = Index::from_file(source).unwrap();
609 assert_eq!(index.size(), 760);
610 for entry in index.entries.values() {
611 println!("{entry}");
612 }
613 }
614
615 #[test]
617 fn test_index_sha256() {
618 let _guard = set_hash_kind_for_test(HashKind::Sha256);
619 let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
620 source.push("tests/data/index/index-9-256");
621
622 let index = Index::from_file(source).unwrap();
623 assert_eq!(index.size(), 9);
624 for entry in index.entries.values() {
625 println!("{entry}");
626 }
627 }
628
629 #[test]
631 fn flags_round_trip_and_length_limit() {
632 let mut flags = Flags {
633 assume_valid: true,
634 extended: true,
635 stage: 2,
636 name_length: 0x0ABC,
637 };
638 let packed: u16 = (&flags).try_into().expect("should pack");
639 let unpacked = Flags::from(packed);
640 assert_eq!(unpacked.assume_valid, flags.assume_valid);
641 assert_eq!(unpacked.extended, flags.extended);
642 assert_eq!(unpacked.stage, flags.stage);
643 assert_eq!(unpacked.name_length, flags.name_length);
644
645 flags.name_length = 0x1FFF;
646 let overflow: Result<u16, _> = (&flags).try_into();
647 assert!(overflow.is_err(), "length overflow should err");
648 }
649
650 #[test]
652 fn index_entry_new_from_blob_populates_fields() {
653 let hash = ObjectHash::from_bytes(&[0u8; 20]).unwrap();
654 let entry = IndexEntry::new_from_blob("file.txt".to_string(), hash, 42);
655 assert_eq!(entry.name, "file.txt");
656 assert_eq!(entry.size, 42);
657 assert_eq!(entry.hash, hash);
658 assert_eq!(entry.flags.name_length, "file.txt".len() as u16);
659 assert_eq!(entry.mode, 0o100644);
660 }
661
662 #[test]
664 fn index_add_and_query_helpers() {
665 let _guard = set_hash_kind_for_test(HashKind::Sha1);
666 let mut index = Index::new();
667 let hash = ObjectHash::from_bytes(&[1u8; 20]).unwrap();
668 let entry = IndexEntry::new_from_blob("a/b.txt".to_string(), hash, 10);
669 index.add(entry);
670
671 let got = index.get("a/b.txt", 0).expect("entry exists");
673 assert_eq!(got.hash, hash);
674
675 let tracked = index.tracked_entries(0);
677 assert_eq!(tracked.len(), 1);
678 let files = index.tracked_files();
679 assert_eq!(files, vec![PathBuf::from("a/b.txt")]);
680
681 assert!(index.contains_dir_file("a"));
683 assert!(!index.contains_dir_file("a/b.txt"));
684
685 let removed = index.remove_dir_files("a");
687 assert_eq!(removed, vec!["a/b.txt".to_string()]);
688 assert!(index.get("a/b.txt", 0).is_none());
689 }
690
691 #[test]
693 fn check_header_validation() {
694 let mut valid = Cursor::new(b"DIRC\0\0\0\x02\0\0\0\0".to_vec());
696 let entries = Index::check_header(&mut valid).expect("valid header");
697 assert_eq!(entries, 0);
698
699 let mut bad_magic = Cursor::new(b"XXXX\0\0\0\x02\0\0\0\0".to_vec());
701 assert!(Index::check_header(&mut bad_magic).is_err());
702
703 let mut bad_version = Cursor::new(b"DIRC\0\0\0\x01\0\0\0\0".to_vec());
705 assert!(Index::check_header(&mut bad_version).is_err());
706 }
707
708 #[test]
710 fn test_index_to_file() {
711 let temp_dir = tempfile::tempdir().unwrap();
712 let temp_path = temp_dir.path().join("index-760");
713
714 let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
715 source.push("tests/data/index/index-760");
716
717 let index = Index::from_file(source).unwrap();
718 index.to_file(&temp_path).unwrap();
719 let new_index = Index::from_file(temp_path).unwrap();
720 assert_eq!(index.size(), new_index.size());
721 }
722
723 #[test]
725 fn test_index_entry_create() {
726 let _guard = set_hash_kind_for_test(HashKind::Sha1);
727 let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
728 source.push("Cargo.toml");
729
730 let file = Path::new(source.as_path()); let hash = ObjectHash::from_bytes(&[0; 20]).unwrap();
732 let workdir = Path::new("../");
733 let entry = IndexEntry::new_from_file(file, hash, workdir).unwrap();
734 println!("{entry}");
735 }
736
737 #[test]
739 fn test_index_entry_create_sha256() {
740 let _guard = set_hash_kind_for_test(HashKind::Sha256);
741 let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
742 source.push("Cargo.toml");
743
744 let file = Path::new(source.as_path());
745 let hash = ObjectHash::from_bytes(&[0u8; 32]).unwrap();
746 let workdir = Path::new("../");
747 let entry = IndexEntry::new_from_file(file, hash, workdir).unwrap();
748 println!("{entry}");
749 }
750}