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